Files

38 lines
5.1 KiB
JavaScript
Raw Permalink Normal View History

2026-10-13 10:33:31 +02:00
'use strict';
// Isolated feasibility harness: never loaded by the managed browser.
const fs=require('node:fs'),os=require('node:os'),path=require('node:path'),assert=require('node:assert/strict'),crypto=require('node:crypto');
if(!process.versions.electron){
const {spawn}=require('node:child_process');const env={...process.env};delete env.ELECTRON_RUN_AS_NODE;
const child=spawn(require('electron'),[__filename],{env,windowsHide:true,stdio:'inherit'});const timer=setTimeout(()=>child.kill(),60000);child.once('exit',code=>{clearTimeout(timer);process.exit(code??1);});
}else{
const {app,BrowserWindow,session}=require('electron');app.disableHardwareAcceleration();app.enableSandbox();
const owned=fs.mkdtempSync(path.join(os.tmpdir(),'safetynet-upload-probe-'));app.setPath('userData',owned);
const {createServer}=require('node:http');let server,win;const report={electron:process.versions.electron,productionEnabled:false};
app.whenReady().then(async()=>{
const original=Buffer.from('PUBLIC IMMUTABLE FILE CONTENT'),changed=Buffer.from('SECRET MUTATED FILE CONTENT!!');const file=path.join(owned,'fixture.txt');fs.writeFileSync(file,original);
const received=[];server=createServer(async(req,res)=>{const chunks=[];for await(const chunk of req)chunks.push(chunk);received.push({url:req.url,headers:req.headers,bytes:Buffer.concat(chunks)});if(req.url==='/redirect'){res.writeHead(307,{location:'/redirect-target'});res.end();return;}res.setHeader('content-type','text/html');res.end('<input id="file" type="file"><script>async function upload(){let f=new FormData();f.append("document",document.querySelector("input").files[0]);return fetch("/upload",{method:"POST",body:f}).then(r=>r.text());}</script>');}).listen(0,'127.0.0.1');await new Promise(r=>server.once('listening',r));
const origin=`http://127.0.0.1:${server.address().port}`,ses=session.fromPartition('upload-proof');let snapshot;
ses.protocol.handle('http',async request=>{
if(new URL(request.url).origin!==origin)return new Response('Denied',{status:403});
const body=request.body?Buffer.from(await request.arrayBuffer()):undefined;
if(request.url.includes('/cookie-')){report.requestCredentials??={};report.requestCredentials[new URL(request.url).pathname]=request.credentials;report.requestCookiePresence??={};report.requestCookiePresence[new URL(request.url).pathname]=request.headers.has('cookie');}
if(request.method==='POST'){snapshot=body;report.requestBodyAvailable=Boolean(body?.length);report.initiatorOrigin=request.initiatorOrigin||null;fs.writeFileSync(file,changed);report.mutatedAfterSnapshot=true;}
try{return await ses.fetch(request.url,{method:request.method,headers:request.headers,body:body?new Blob([body]):undefined,redirect:'error',bypassCustomProtocolHandlers:true});}catch{return Response.error();}
});
win=new BrowserWindow({show:false,webPreferences:{session:ses,sandbox:true,contextIsolation:true,nodeIntegration:false}});await win.loadURL(origin);
win.webContents.debugger.attach('1.3');const {root}=await win.webContents.debugger.sendCommand('DOM.getDocument');const {nodeId}=await win.webContents.debugger.sendCommand('DOM.querySelector',{nodeId:root.nodeId,selector:'#file'});await win.webContents.debugger.sendCommand('DOM.setFileInputFiles',{nodeId,files:[file]});
await win.webContents.executeJavaScript('upload()');const upload=received.find(r=>r.url==='/upload');assert(upload);assert(snapshot);assert.deepEqual(upload.bytes,snapshot);assert(upload.bytes.includes(original));assert(!upload.bytes.includes(changed));report.receiverSha256=crypto.createHash('sha256').update(upload.bytes).digest('hex');report.snapshotSha256=crypto.createHash('sha256').update(snapshot).digest('hex');report.exactBytes=true;
await ses.cookies.set({url:origin,name:'synthetic_session',value:'fixture',sameSite:'strict'});
await win.webContents.executeJavaScript('fetch("/cookie-include",{credentials:"include"}).then(r=>r.text())');
await win.webContents.executeJavaScript('fetch("/cookie-omit",{credentials:"omit"}).then(r=>r.text())');
report.includeCookie=Boolean(received.find(r=>r.url==='/cookie-include').headers.cookie);
report.omitCookie=Boolean(received.find(r=>r.url==='/cookie-omit').headers.cookie);
report.credentialsPreserved=report.includeCookie&&!report.omitCookie;
// A known unsafe expansion is asserted here so the evidence cannot be mistaken
// for approval to ship the transparent bridge. No real credentials are used.
assert.equal(report.credentialsPreserved,false,'Reassess Electron credential behavior before integration');
report.redirectDenied=await win.webContents.executeJavaScript('fetch("/redirect").then(()=>false,()=>true)');
assert.equal(report.redirectDenied,true);assert.equal(received.some(r=>r.url==='/redirect-target'),false);
fs.mkdirSync(path.resolve('artifacts'),{recursive:true});fs.writeFileSync('artifacts/immutable-upload-probe.json',JSON.stringify(report,null,2));console.log(JSON.stringify(report));
}).catch(error=>{console.error(error);process.exitCode=1;}).finally(async()=>{win?.destroy();if(server){server.closeAllConnections();await new Promise(r=>server.close(r));}app.exit(process.exitCode||0);});
}