32 lines
4.2 KiB
JavaScript
32 lines
4.2 KiB
JavaScript
import test from 'node:test';
|
|||
|
|
import assert from 'node:assert/strict';
|
||
|
|
import {createRequire} from 'node:module';
|
||
|
|
import {EventEmitter} from 'node:events';
|
||
|
|
import vm from 'node:vm';
|
||
|
|
const require=createRequire(import.meta.url);
|
||
|
|
const {visualDocumentProbe,probeVisualContext}=require('../apps/browser/agent-visual-probe.cjs');
|
||
|
|
const {attachPageClipboardGuard,guardedVisualStructure,disposePageClipboardGuard}=require('../apps/browser/page-clipboard-guard.cjs');
|
||
|
|
|
||
|
|
function node(tag='DIV',attributes={},visible=true){return {tagName:tag,localName:tag.toLowerCase(),type:attributes.type||'',hidden:!visible,parentElement:null,getAttribute:key=>attributes[key]??null,hasAttribute:key=>Object.hasOwn(attributes,key),getBoundingClientRect:()=>({left:1,top:1,right:10,bottom:10,width:9,height:9})};}
|
||
|
|
function probe(nodes){let change;const watched=new Map();const realm={location:{href:'https://example.com'},document:{readyState:'complete',querySelectorAll:selector=>selector==='video'?[]:nodes,getAnimations:()=>[]},crypto:{randomUUID:()=> 'private-document'},MutationObserver:class{constructor(fn){change=fn;this.records=[];}observe(root){watched.set(root,this);}takeRecords(){return this.records.splice(0);}disconnect(){}},addEventListener(){},innerWidth:1280,innerHeight:720,scrollX:0,scrollY:0,devicePixelRatio:1,visualViewport:{scale:1},getComputedStyle:()=>({display:'block',visibility:'visible',opacity:'1'})};vm.createContext(realm);return {run:()=>vm.runInContext(`(${visualDocumentProbe.toString()})('https://example.com')`,realm),change:()=>change(),mutate:root=>watched.get(root).records.push({})};}
|
||
|
|
test('isolated probe rejects credential controls even offscreen and probes open shadow content',()=>{
|
||
|
|
assert.equal(probe([node()]).run().safe,true);
|
||
|
|
for(const attributes of [{type:'password'},{autocomplete:'section-login current-password'},{name:'otp'},{id:'api_key'}])assert.equal(probe([node('INPUT',attributes,false)]).run().safe,false);
|
||
|
|
const host=node();host.shadowRoot={querySelectorAll:()=>[node('INPUT',{type:'password'})]};assert.equal(probe([host]).run().safe,false);
|
||
|
|
assert.equal(probe([node('IFRAME')]).run().safe,false);
|
||
|
|
const p=probe([node()]),first=p.run();p.change();assert.notEqual(p.run().revision,first.revision);
|
||
|
|
});
|
||
|
|
|
||
|
|
async function guard(t,root){const api=new EventEmitter();let attached=false;api.isAttached=()=>attached;api.attach=()=>{attached=true;};api.detach=()=>{attached=false;api.emit('detach');};api.sendCommand=async method=>method==='Page.addScriptToEvaluateOnNewDocument'?{identifier:'private'}:method==='DOM.getDocument'?{root}:{};const contents={debugger:api,isDestroyed:()=>false};await attachPageClipboardGuard(contents,()=>{});t.after(()=>disposePageClipboardGuard(contents));return contents;}
|
||
|
|
const dom=(extra={})=>({nodeName:'DIV',nodeValue:'',attributes:[],...extra});
|
||
|
|
test('private native structure catches closed/declarative shadow roots and refuses budgets/frames',async t=>{
|
||
|
|
for(const root of [dom({shadowRoots:[dom({shadowRootType:'closed'})]}),dom({contentDocument:dom()}),dom({nodeValue:'x'.repeat(1024*1024+1)})])await assert.rejects(guardedVisualStructure(await guard(t,root)));
|
||
|
|
const contents=await guard(t,dom({shadowRoots:[dom({shadowRootType:'open'})]}));assert.equal((await guardedVisualStructure(contents)).safe,true);
|
||
|
|
await assert.rejects(guardedVisualStructure({isDestroyed:()=>false}));
|
||
|
|
});
|
||
|
|
test('native subframes cannot fall back to untrusted page-world probes',async()=>{
|
||
|
|
let executed=false;const frame={isDestroyed:()=>false,framesInSubtree:[{},{}]},contents={mainFrame:frame,isLoading:()=>false,executeJavaScriptInIsolatedWorld:()=>{executed=true;}};
|
||
|
|
await assert.rejects(probeVisualContext({webContents:contents,url:'https://example.com'}));assert.equal(executed,false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('shadow-root mutation and reversal cannot preserve screenshot revision',()=>{const host=node(),root={host:{isConnected:true},querySelectorAll:()=>[]};host.shadowRoot=root;const p=probe([host]),baseline=p.run();p.mutate(root);p.mutate(root);assert.equal(p.run().safe,true);assert.notEqual(p.run().revision,baseline.revision);const existing=node(),q=probe([existing]),before=q.run();existing.shadowRoot={host:{isConnected:true},querySelectorAll:()=>[]};assert.notEqual(q.run().revision,before.revision);});
|