Harden Windows DLP enforcement and Electron runtime packaging
This commit is contained in:
1 parent
e7d778aeac
commit
8a253eda89
120 files changed
+3569
-135
No files matched your search
@@ -0,0 +1,37 @@
|
||||
import test from 'node:test';import assert from 'node:assert/strict';import http from 'node:http';import https from 'node:https';import net from 'node:net';import tls from 'node:tls';import {readFileSync} from 'node:fs';import {createRequire} from 'node:module';
|
||||
const {startLocalDlpRelay}=createRequire(import.meta.url)('../apps/browser/local-dlp-relay.cjs');
|
||||
async function listening(server,t){server.listen(0,'127.0.0.1');await new Promise(r=>server.once('listening',r));t.after(async()=>{server.closeAllConnections?.();await new Promise(r=>server.close(r));});return server.address().port;}
|
||||
function basic(relay){const c=relay.credentials();return 'Basic '+Buffer.from(c.username+':'+c.password).toString('base64');}
|
||||
function get(relay,target,auth=basic(relay)){return new Promise((resolve,reject)=>{const [host,port]=relay.proxyRules.split(':'),req=http.request({host,port,path:target,headers:{host:new URL(target).host,...(auth?{'proxy-authorization':auth}:{})}},res=>{let text='';res.on('data',b=>text+=b);res.on('end',()=>resolve({status:res.statusCode,text}));});req.on('error',reject);req.end();});}
|
||||
function connect(relay,target){return new Promise((resolve,reject)=>{const [host,port]=relay.proxyRules.split(':'),socket=net.connect({host,port});let bytes=Buffer.alloc(0);socket.on('error',reject);socket.on('data',function header(chunk){bytes=Buffer.concat([bytes,chunk]);if(bytes.indexOf('\r\n\r\n')<0)return;socket.removeListener('data',header);if(!bytes.toString().startsWith('HTTP/1.1 200')){socket.destroy();reject(Error('CONNECT refused'));return;}resolve(socket);});socket.once('connect',()=>socket.write(`CONNECT ${target} HTTP/1.1\r\nHost: ${target}\r\nProxy-Authorization: ${basic(relay)}\r\n\r\n`));});}
|
||||
async function fixture(t,overrides={}){const authority={version:1},addresses=[];const relay=await startLocalDlpRelay({authorize:async()=>authority,isCurrent:a=>a===authority,resolveUpstream:async()=> 'DIRECT',resolveAddress:async host=>{addresses.push(host);return '127.0.0.1';},...overrides});t.after(()=>relay.close());return {relay,authority,addresses};}
|
||||
test('private loopback relay authenticates capability and forwards approved HTTP without leaking proxy credentials',async t=>{
|
||||
let hits=0,headers;const port=await listening(http.createServer((req,res)=>{hits++;headers=req.headers;res.end('public receiver');}),t),f=await fixture(t),url=`http://fixture.invalid:${port}/public`;
|
||||
assert.equal((await get(f.relay,url,null)).status,407);assert.equal(hits,0);assert.equal((await get(f.relay,url,'Basic forged')).status,407);
|
||||
const result=await get(f.relay,url);assert.equal(result.status,200);assert.equal(result.text,'public receiver');assert.equal(hits,1);assert.equal(headers['proxy-authorization'],undefined);assert.deepEqual(f.addresses,['fixture.invalid']);assert.equal(f.relay.proxyBypassRules,'<-loopback>');
|
||||
});
|
||||
test('native CONNECT tunnel carries actual TLS application data and remains private',async t=>{
|
||||
const certificate=readFileSync('tests/fixtures/icap/cert.pem'),key=readFileSync('tests/fixtures/icap/key.pem');let hits=0;
|
||||
const port=await listening(https.createServer({cert:certificate,key},(_req,res)=>{hits++;res.end('public TLS receiver');}),t),f=await fixture(t),socket=await connect(f.relay,`127.0.0.1:${port}`);
|
||||
const secure=tls.connect({socket,ca:certificate,servername:'localhost'});let response='';await new Promise((resolve,reject)=>{secure.on('error',reject);secure.once('secureConnect',()=>secure.write('GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n'));secure.on('data',b=>response+=b);secure.once('end',resolve);});assert.match(response,/public TLS receiver/);assert.equal(hits,1);
|
||||
});
|
||||
test('PAC selected upstream proxy is preserved, no target DNS is performed and unsupported SOCKS never falls back',async t=>{
|
||||
let hits=0;const proxyPort=await listening(http.createServer((req,res)=>{hits++;assert.equal(req.url,'http://enterprise.invalid:8080/public');assert.equal(req.headers['proxy-authorization'],'Basic '+Buffer.from('corporate:user-password').toString('base64'));res.end('enterprise forwarded');}),t);
|
||||
const f=await fixture(t,{resolveUpstream:async()=>`PROXY proxy.enterprise.invalid:${proxyPort}; DIRECT`,credentialsForProxy:async()=>({username:'corporate',password:'user-password'})});assert.equal((await get(f.relay,'http://enterprise.invalid:8080/public')).text,'enterprise forwarded');assert.deepEqual(f.addresses,['proxy.enterprise.invalid']);assert.equal(hits,1);
|
||||
const unsupported=await fixture(t,{resolveUpstream:async()=> 'SOCKS5 proxy.enterprise.invalid:1080; DIRECT'});assert.equal((await get(unsupported.relay,'http://enterprise.invalid:8080/public')).status,502);assert.equal(unsupported.addresses.length,0);
|
||||
});
|
||||
test('policy invalidation during async resolution denies transmission and existing tunnels are closed',async t=>{
|
||||
let release,started;const begun=new Promise(r=>started=r),waiting=new Promise(r=>release=r);let hits=0;
|
||||
const port=await listening(http.createServer((_req,res)=>{hits++;res.end('should not release');}),t),f=await fixture(t,{resolveUpstream:async()=>{started();return waiting;}});
|
||||
const pending=get(f.relay,`http://fixture.invalid:${port}/`);await begun;f.relay.invalidate();release('DIRECT');await assert.rejects(pending);assert.equal(hits,0);
|
||||
const sockets=new Set(),echo=net.createServer(socket=>{sockets.add(socket);socket.on('error',()=>{});socket.on('data',data=>socket.write(data));socket.once('close',()=>sockets.delete(socket));}),echoPort=await listening(echo,t),g=await fixture(t),socket=await connect(g.relay,`fixture.invalid:${echoPort}`);await new Promise(resolve=>{socket.once('data',data=>{assert.equal(data.toString(),'public echo');resolve();});socket.write('public echo');});const ended=new Promise(r=>socket.once('close',r));g.relay.invalidate();await ended;assert.equal(g.relay.status().connections,0);for(const s of sockets)s.destroy();
|
||||
});
|
||||
test('resolver authority errors and nonnumeric addresses fail closed before target connection',async t=>{
|
||||
const f=await fixture(t,{resolveAddress:async()=> 'unapproved.example'});assert.equal((await get(f.relay,'http://target.invalid:8080/')).status,502);
|
||||
const g=await fixture(t,{isCurrent:()=>{throw Error('Policy unavailable');}});assert.equal((await get(g.relay,'http://target.invalid:8080/')).status,502);assert.equal(g.addresses.length,0);
|
||||
});
|
||||
test('upstream proxy challenge does not impersonate local relay authentication and resolution hangs expire',async t=>{
|
||||
const port=await listening(http.createServer((_req,res)=>{res.writeHead(407,{'proxy-authenticate':'Basic realm="untrusted upstream"'});res.end('do not expose authentication data');}),t);
|
||||
const f=await fixture(t,{resolveUpstream:async()=>`PROXY proxy.invalid:${port}`});const result=await get(f.relay,'http://target.invalid:8080/');assert.equal(result.status,502);assert.equal(result.text,'');
|
||||
const g=await fixture(t,{timeoutMs:100,resolveUpstream:async()=>new Promise(()=>{})});await assert.rejects(get(g.relay,'http://target.invalid:8080/'));assert.equal(g.addresses.length,0);
|
||||
});
|
||||
Reference in new issue
Block a user