Harden Windows DLP enforcement and Electron runtime packaging

This commit is contained in:
Karl committed 2026-10-13 23:19:22 +02:00
1 parent e7d778aeac
commit 8a253eda89
120 files changed
+3569 -135

No files matched your search

+61
View File
@@ -0,0 +1,61 @@
'use strict';
const http=require('node:http'),net=require('node:net'),tls=require('node:tls');
const {randomBytes,timingSafeEqual}=require('node:crypto');
function endpoint(value){
const u=new URL(value);if(!['http:','https:'].includes(u.protocol)||u.username||u.password||u.hash)throw Error('relay-target-refused');
return {url:u.href,scheme:u.protocol,host:u.hostname.replace(/^\[|\]$/g,''),port:Number(u.port||(u.protocol==='https:'?443:80)),path:u.pathname+u.search};
}
function upstream(value){
// Honor the first selected enterprise route. Never downgrade failed or
// unsupported routes to DIRECT, even when a PAC result lists fallback entries.
const first=String(value).split(';')[0].trim();if(first==='DIRECT')return {direct:true};
const m=/^(PROXY|HTTPS) (\[[a-fA-F0-9:]+\]|[a-zA-Z0-9.-]+):([0-9]{1,5})$/.exec(first);
if(!m||Number(m[3])<1||Number(m[3])>65535)throw Error('relay-upstream-unsupported');
return {direct:false,tls:m[1]==='HTTPS',host:m[2].replace(/^\[|\]$/g,''),port:Number(m[3])};
}
async function startLocalDlpRelay({authorize,isCurrent,resolveUpstream,resolveAddress,credentialsForProxy=async()=>null,timeoutMs=30000,maxConnections=128,maxBodyBytes=10485760}={}){
if([authorize,isCurrent,resolveUpstream,resolveAddress,credentialsForProxy].some(fn=>typeof fn!=='function')||!Number.isInteger(timeoutMs)||timeoutMs<100||timeoutMs>60000||!Number.isInteger(maxConnections)||maxConnections<1||maxConnections>256)throw TypeError('Relay authority callbacks required');
const username='safetynet-'+randomBytes(16).toString('hex'),password=randomBytes(32).toString('base64url'),authorization='Basic '+Buffer.from(username+':'+password).toString('base64');
const sockets=new Set(),clients=new Set();let closed=false,epoch=1;
const current=context=>{try{return !closed&&context.epoch===epoch&&isCurrent(context.authority)===true;}catch{return false;}};
const bounded=work=>new Promise((resolve,reject)=>{const timer=setTimeout(()=>reject(Error('relay-operation-timeout')),timeoutMs);Promise.resolve(work).then(value=>{clearTimeout(timer);resolve(value);},error=>{clearTimeout(timer);reject(error);});});
function check(context){if(!current(context))throw Error('relay-authority-changed');}
function track(socket){sockets.add(socket);socket.on('error',()=>{});socket.once('close',()=>sockets.delete(socket));socket.setTimeout(timeoutMs,()=>socket.destroy());return socket;}
function authenticated(req){const supplied=req.headers['proxy-authorization'];return req.socket.remoteAddress==='127.0.0.1'&&typeof supplied==='string'&&supplied.length===authorization.length&&timingSafeEqual(Buffer.from(supplied),Buffer.from(authorization));}
function headers(req){const h={...req.headers},nominated=String(req.headers.connection||'').split(',').map(value=>value.trim().toLowerCase());for(const key of [...nominated,'proxy-authorization','proxy-authenticate','proxy-connection','connection','keep-alive','upgrade','te','trailer','transfer-encoding'])delete h[key];return h;}
async function route(target,method){const generation=epoch,authority=await bounded(authorize(target.url,{method}));if(!authority)throw Error('relay-authority-refused');const context={authority,epoch:generation};check(context);const selected=upstream(await bounded(resolveUpstream(target.url)));check(context);return {context,selected};}
async function address(host,context){const ip=await bounded(resolveAddress(host,context.authority));check(context);if(typeof ip!=='string'||!net.isIP(ip))throw Error('relay-address-unapproved');return ip;}
async function dial(host,port,secure,context){const ip=await address(host,context);return new Promise((resolve,reject)=>{let socket;try{check(context);socket=track(secure?tls.connect({host:ip,port,servername:host,rejectUnauthorized:true}):net.connect({host:ip,port}));socket.once(secure?'secureConnect':'connect',()=>{try{check(context);resolve(socket);}catch(e){socket.destroy();reject(e);}});socket.once('error',reject);socket.once('close',()=>reject(Error('relay-socket-closed')));}catch(e){reject(e);}});}
async function proxyAuth(selected,context){const credentials=await bounded(credentialsForProxy(selected));check(context);if(!credentials)return null;if(typeof credentials.username!=='string'||typeof credentials.password!=='string'||credentials.username.length>1024||credentials.password.length>1024||credentials.username.includes(':')||/[\r\n]/.test(credentials.username+credentials.password))throw Error('relay-proxy-credentials-invalid');return 'Basic '+Buffer.from(credentials.username+':'+credentials.password).toString('base64');}
async function tunnel(target,selected,context){
const socket=await dial(selected.direct?target.host:selected.host,selected.direct?target.port:selected.port,!selected.direct&&selected.tls,context);
if(selected.direct)return socket;
const auth=await proxyAuth(selected,context),authority=(net.isIP(target.host)===6?'['+target.host+']':target.host)+':'+target.port;
check(context);socket.write(`CONNECT ${authority} HTTP/1.1\r\nHost: ${authority}\r\n${auth?'Proxy-Authorization: '+auth+'\r\n':''}\r\n`);
return new Promise((resolve,reject)=>{let bytes=Buffer.alloc(0);const data=chunk=>{bytes=Buffer.concat([bytes,chunk]);if(bytes.length>32768){socket.destroy();reject(Error('relay-proxy-response-limit'));return;}const end=bytes.indexOf('\r\n\r\n');if(end<0)return;socket.removeListener('data',data);try{check(context);if(!/^HTTP\/1\.[01] 200(?: |\r)/.test(bytes.subarray(0,end).toString('latin1')))throw Error('relay-proxy-connect-refused');const tail=bytes.subarray(end+4);if(tail.length)socket.unshift(tail);resolve(socket);}catch(e){socket.destroy();reject(e);}};socket.on('data',data);socket.once('error',reject);socket.once('close',()=>reject(Error('relay-proxy-connect-closed')));socket.once('end',()=>reject(Error('relay-proxy-connect-ended')));});
}
const server=http.createServer(async(req,res)=>{
if(!authenticated(req)){res.writeHead(407,{'Proxy-Authenticate':'Basic realm="S Browser private relay"'});res.end();req.resume();return;}
let remote;try{
const target=endpoint(req.url);if(target.scheme!=='http:')throw Error('relay-plain-scheme-refused');const {context,selected}=await route(target,req.method);
remote=await dial(selected.direct?target.host:selected.host,selected.direct?target.port:selected.port,!selected.direct&&selected.tls,context);check(context);
const outgoingHeaders=headers(req),auth=selected.direct?null:await proxyAuth(selected,context);outgoingHeaders.host=new URL(target.url).host;if(auth)outgoingHeaders['proxy-authorization']=auth;
const agent=new http.Agent({keepAlive:false});agent.createConnection=()=>remote;
const outgoing=http.request({method:req.method,path:selected.direct?target.path:target.url,headers:outgoingHeaders,agent},incoming=>{try{check(context);if(!selected.direct&&incoming.statusCode===407){incoming.destroy();agent.destroy();res.writeHead(502);res.end();return;}res.writeHead(incoming.statusCode,headers(incoming));incoming.on('data',chunk=>{if(!current(context)){incoming.destroy();res.destroy();return;}if(!res.write(chunk))incoming.pause();});res.on('drain',()=>incoming.resume());incoming.on('end',()=>{res.end();agent.destroy();});incoming.on('error',()=>res.destroy());}catch{incoming.destroy();res.destroy();}});
outgoing.on('error',()=>{remote.destroy();if(!res.headersSent)res.writeHead(502);res.end();});let size=0;
req.on('data',chunk=>{size+=chunk.length;if(size>maxBodyBytes||!current(context)){outgoing.destroy();req.destroy();return;}if(!outgoing.write(chunk))req.pause();});outgoing.on('drain',()=>req.resume());req.on('end',()=>outgoing.end());req.on('error',()=>outgoing.destroy());
}catch{remote?.destroy();if(!res.headersSent)res.writeHead(502);res.end();req.resume();}
});
server.on('connect',async(req,client,head)=>{
if(!authenticated(req)){client.end('HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm="S Browser private relay"\r\nConnection: close\r\n\r\n');return;}
let remote;try{if(!/^(?:\[[a-fA-F0-9:]+\]|[a-zA-Z0-9.-]+):[0-9]{1,5}$/.test(req.url))throw Error('relay-connect-target-refused');const target=endpoint('https://'+req.url),{context,selected}=await route(target,'CONNECT');remote=await tunnel(target,selected,context);check(context);if(client.destroyed){remote.destroy();return;}client.write('HTTP/1.1 200 Connection Established\r\n\r\n');if(head.length)remote.write(head);
client.on('data',chunk=>{if(!current(context)){client.destroy();remote.destroy();return;}if(!remote.write(chunk))client.pause();});remote.on('drain',()=>client.resume());remote.on('data',chunk=>{if(!current(context)){client.destroy();remote.destroy();return;}if(!client.write(chunk))remote.pause();});client.on('drain',()=>remote.resume());client.once('end',()=>remote.end());remote.once('end',()=>client.end());client.once('close',()=>remote.destroy());remote.once('close',()=>client.destroy());
}catch{remote?.destroy();client.end('HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n');}
});
server.on('connection',socket=>{if(closed||socket.remoteAddress!=='127.0.0.1'||clients.size>=maxConnections){socket.destroy();return;}clients.add(socket);track(socket);socket.once('close',()=>clients.delete(socket));});server.on('clientError',(_error,socket)=>socket.destroy());
server.headersTimeout=timeoutMs;server.requestTimeout=timeoutMs;server.keepAliveTimeout=1000;
await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(0,'127.0.0.1',resolve);});
return {proxyRules:'127.0.0.1:'+server.address().port,proxyBypassRules:'<-loopback>',credentials:()=>({username,password}),invalidate(){epoch++;for(const socket of sockets)socket.destroy();},async close(){closed=true;epoch++;for(const socket of sockets)socket.destroy();await new Promise(resolve=>server.close(resolve));},status:()=>({closed,connections:[...clients].filter(socket=>!socket.destroyed).length})};
}
module.exports={startLocalDlpRelay};