import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import vm from 'node:vm'; const source=fs.readFileSync(new URL('../apps/browser/site-preload.cjs',import.meta.url),'utf8'); function harness(enabled,options={}){ const listeners=new Map(),ipcListeners=new Map(),timers=[],requests=[],sent=[],observers=[]; const window={stop:()=>{},addEventListener:(name,handler,capture)=>{assert.equal(capture===true||capture===undefined,true);listeners.set(name,[...(listeners.get(name)||[]),handler]);}}; const ipcRenderer={on:(channel,callback)=>ipcListeners.set(channel,callback),sendSync:channel=>{assert.equal(channel,'managed:visual-policy');return {enabled};},send:(channel,payload)=>sent.push({channel,payload}),invoke:(channel,payload)=>new Promise(resolve=>requests.push({channel,payload,resolve}))}; const location={href:options.url||'https://example.test/login'}; class MutationObserver{constructor(callback){this.callback=callback;observers.push(this);}observe(_element,settings){this.settings=settings;}} class Document{}Document.prototype.execCommand=()=>true; const context={require:name=>{assert.equal(name,'electron');return {ipcRenderer,contextBridge:{executeInMainWorld:({func})=>func()}};},Document,DOMException,process:{sandboxed:true,contextIsolated:true,isMainFrame:true},window,location,document:{body:{innerText:'Public page content'},documentElement:{}},crypto:options.crypto||{randomUUID:()=> 'opaque-document-fixture-token'},MutationObserver,setTimeout:(callback,delay)=>{timers.push({callback,delay});return timers.length;}}; vm.runInNewContext(source,context,{filename:'site-preload.cjs'}); function dispatch(name,credential=true,inputType){const result={prevented:false,stopped:false};const target={closest:()=>credential?target:null};for(const listener of listeners.get(name)||[]){listener({target,inputType,preventDefault:()=>result.prevented=true,stopImmediatePropagation:()=>result.stopped=true});if(result.stopped)break;}return result;} function domReady(){for(const listener of listeners.get('DOMContentLoaded')||[])listener();} function mutate(attribute){assert.ok(observers.length);for(const observer of observers){if(!attribute||observer.settings.attributes&&(!observer.settings.attributeFilter||observer.settings.attributeFilter.includes(attribute)))observer.callback([]);}} function configure(nextEnabled,token){assert.ok(ipcListeners.has('managed:visual-config'));ipcListeners.get('managed:visual-config')({}, {enabled:nextEnabled,token});} function timer(delay){const index=timers.findIndex(t=>t.delay===delay);if(index<0)return false;const item=timers.splice(index,1)[0];item.callback();return true;} return {context,location,requests,sent,timers,dispatch,domReady,mutate,timer,configure}; } const settle=()=>new Promise(resolve=>setImmediate(resolve)); test('disabled visual policy preserves input and submits without requesting visual approval',()=>{ const f=harness(false);f.domReady(); for(const event of ['beforeinput','submit','pointerdown'])assert.deepEqual(f.dispatch(event),{prevented:false,stopped:false}); assert.equal(f.timer(150),false);assert.equal(f.requests.length,0); assert.equal(f.timer(350),true);assert.equal(f.sent[0].channel,'managed:page-content'); }); test('enabled credential input, paste and submit are held until current document approval',async()=>{ const f=harness(true);f.domReady(); for(const event of ['beforeinput','paste','submit'])assert.deepEqual(f.dispatch(event),{prevented:true,stopped:true}); assert.equal(f.timer(150),true);assert.equal(f.requests.length,1); const request=f.requests[0];assert.equal(request.channel,'managed:visual-check');assert.equal(request.payload.url,f.location.href);assert.ok(request.payload.revision>0); request.resolve({decision:'allow'});await settle(); for(const event of ['beforeinput','submit'])assert.deepEqual(f.dispatch(event),{prevented:false,stopped:false}); }); test('DOM mutation invalidates approval synchronously before a new inspection completes',async()=>{ const f=harness(true);f.domReady();f.timer(150);f.requests[0].resolve({decision:'allow'});await settle(); assert.equal(f.dispatch('beforeinput').prevented,false); f.mutate(); assert.equal(f.dispatch('beforeinput').prevented,true,'A changed credential form must not receive input during inspection'); assert.equal(f.dispatch('submit').prevented,true); }); test('stale successful inspection cannot release a form changed while inspection was pending',async()=>{ const f=harness(true);f.domReady();f.timer(150);const first=f.requests[0]; f.mutate();f.timer(150);const second=f.requests[1];assert.ok(second.payload.revision>first.payload.revision); first.resolve({decision:'allow'});await settle(); assert.equal(f.dispatch('beforeinput').prevented,true,'An allow result for the previous form revision must be discarded'); // The held paste itself invalidates the outstanding second revision as another attempt. second.resolve({decision:'allow'});await settle();assert.equal(f.dispatch('submit').prevented,true); f.timer(150);const latest=f.requests.at(-1);latest.resolve({decision:'allow'});await settle(); assert.equal(f.dispatch('beforeinput').prevented,false); }); test('changed document URL discards an outstanding allow result and exposes no page bridge',async()=>{ const f=harness(true);f.domReady();f.timer(150);f.location.href='https://replacement.test/login';f.requests[0].resolve({decision:'allow'});await settle(); assert.equal(f.dispatch('beforeinput').prevented,true); assert.equal(f.context.window.safetynet,undefined);assert.equal(f.context.window.ipcRenderer,undefined); assert.ok(f.requests.every(request=>Object.keys(request.payload).sort().join(',')==='revision,token,url')); }); test('live policy enable holds input immediately and acknowledges before asynchronous inspection',async()=>{ const f=harness(false);f.domReady();assert.equal(f.dispatch('beforeinput').prevented,false); f.configure(true,'configuration-1');assert.equal(f.dispatch('beforeinput').prevented,true); assert.ok(f.sent.some(item=>item.channel==='managed:visual-configured'&&item.payload==='configuration-1')); f.timer(150);f.requests.at(-1).resolve({decision:'allow'});await settle();assert.equal(f.dispatch('beforeinput').prevented,false); f.configure(false,'configuration-2');assert.equal(f.dispatch('beforeinput').prevented,false);assert.equal(f.dispatch('submit').prevented,false); assert.ok(f.sent.some(item=>item.channel==='managed:visual-configured'&&item.payload==='configuration-2')); }); test('live disable and re-enable invalidates outstanding old approval',async()=>{ const f=harness(true);f.domReady();f.timer(150);const stale=f.requests[0]; f.configure(false,'off');assert.equal(f.dispatch('beforeinput').prevented,false); f.configure(true,'on');stale.resolve({decision:'allow'});await settle();assert.equal(f.dispatch('beforeinput').prevented,true); }); test('credential marker attributes invalidate approval even without visual text changes',async()=>{ for(const attribute of ['name','id','autocomplete','aria-hidden','disabled']){ const f=harness(true);f.domReady();f.timer(150);f.requests[0].resolve({decision:'allow'});await settle();assert.equal(f.dispatch('beforeinput').prevented,false); f.mutate(attribute);assert.equal(f.dispatch('beforeinput').prevented,true,`${attribute} changed a credential form without changing its visible text`); } }); test('legacy clipboard primitive remains frozen and unsupported page transfer events are always denied',()=>{ const f=harness(false);const descriptor=Object.getOwnPropertyDescriptor(f.context.Document.prototype,'execCommand'); assert.equal(descriptor.configurable,false);assert.equal(descriptor.writable,false); for(const operation of ['copy','cut','paste','PasteAndMatchStyle','PasteGlobalSelection'])assert.equal(descriptor.value(operation),false); assert.equal(descriptor.value('bold'),true); for(const event of ['copy','cut','paste','dragstart','dragover','drop'])assert.equal(f.dispatch(event).prevented,true); for(const type of ['insertFromPaste','insertFromDrop','deleteByCut'])assert.equal(f.dispatch('beforeinput',true,type).prevented,true); let coerced=false;assert.equal(descriptor.value({toString:()=>{coerced=true;return 'copy';}}),false);assert.equal(coerced,false); for(const operation of ['open','write','writeln']){ const replacement=Object.getOwnPropertyDescriptor(f.context.Document.prototype,operation);assert.equal(replacement.configurable,false);assert.equal(replacement.writable,false); assert.throws(()=>replacement.value(),error=>error.name==='SecurityError'); } }); test('document listener reset and page tampering with Reflect/RegExp cannot restore legacy clipboard commands',()=>{ const f=harness(false); // Document.open clears listeners without creating a new JS realm; retain only the frozen primitive. const result=vm.runInNewContext(`(()=>{window.addEventListener=()=>{};Reflect.apply=()=>true;RegExp.prototype.test=()=>false;try{Document.prototype.execCommand=()=>true;}catch{};return {copy:Document.prototype.execCommand('copy'),paste:Document.prototype.execCommand('paste'),bold:Document.prototype.execCommand('bold'),locked:Object.getOwnPropertyDescriptor(Document.prototype,'execCommand').configurable===false};})()`,f.context); assert.equal(result.copy,false);assert.equal(result.paste,false);assert.equal(result.bold,true);assert.equal(result.locked,true); }); test('HTTP insecure context without randomUUID still monitors content and uses random document token',()=>{ let randomCalls=0; const f=harness(true,{url:'http://proxy-fixture.test/',crypto:{getRandomValues:bytes=>{randomCalls++;for(let i=0;iitem.channel==='managed:page-content').payload)),{text:'Public page content'}); f.timer(150);assert.match(f.requests[0].payload.token,/^[a-f0-9]{32}$/);assert.equal(f.requests[0].payload.url,'http://proxy-fixture.test/'); f.context.document.body.innerText='Changed public page';f.mutate('name');f.timer(350); assert.equal(f.sent.filter(item=>item.channel==='managed:page-content').at(-1).payload.text,'Changed public page'); });