Add inspected agent viewport export with policy-bound final release
This commit is contained in:
1 parent
c8b6ff9cdd
commit
e7d778aeac
24 files changed
+483
-32
No files matched your search
@@ -0,0 +1,62 @@
|
||||
'use strict';
|
||||
|
||||
// Runs only in a private isolated world. No bridge, page callback or caller code.
|
||||
function visualDocumentProbe(expectedURL) {
|
||||
if (location.href !== expectedURL || document.readyState !== 'complete') throw Error('Visual probe refused');
|
||||
const key='__safetynetVisualProbeV1';
|
||||
let state=globalThis[key];
|
||||
if (!state) {
|
||||
state={id:crypto.randomUUID(),revision:0,roots:new WeakSet(),watches:[]};
|
||||
const changed=()=>{state.revision++;};
|
||||
state.watch=root=>{
|
||||
if(state.roots.has(root))return;
|
||||
if(state.watches.length>=129)throw Error('Visual probe refused');
|
||||
const observer=new MutationObserver(changed);
|
||||
observer.observe(root,{subtree:true,childList:true,characterData:true,attributes:true});
|
||||
state.roots.add(root);state.watches.push({root,observer});
|
||||
if(root!==document)changed();
|
||||
};
|
||||
state.watch(document);
|
||||
for (const name of ['input','change','scroll','resize']) addEventListener(name,changed,true);
|
||||
Object.defineProperty(globalThis,key,{value:state,writable:false,configurable:false});
|
||||
}
|
||||
for(const watch of [...state.watches]){
|
||||
if(watch.root!==document&&watch.root.host?.isConnected===false){watch.observer.disconnect();state.roots.delete(watch.root);state.watches.splice(state.watches.indexOf(watch),1);state.revision++;}
|
||||
else if(watch.observer.takeRecords().length)state.revision++;
|
||||
}
|
||||
const forbidden=/password|passwd|secret|token|credential|api.?key|auth|one.?time|otp|credit.?card|cc-number|cc-csc/i;
|
||||
let safe=true,visited=0;
|
||||
const visible=e=>{
|
||||
for(let n=e;n;n=n.parentElement){const s=getComputedStyle(n);if(s.display==='none'||s.visibility==='hidden'||s.opacity==='0'||n.hidden)return false;}
|
||||
const r=e.getBoundingClientRect();return r.width>0&&r.height>0&&r.bottom>0&&r.right>0&&r.left<innerWidth&&r.top<innerHeight;
|
||||
};
|
||||
function walk(root) {
|
||||
state.watch(root);
|
||||
for(const e of root.querySelectorAll('*')) {
|
||||
if(++visited>20000){safe=false;return;}
|
||||
if(['IFRAME','FRAME','OBJECT','EMBED'].includes(e.tagName)){safe=false;return;}
|
||||
// Credential-bearing controls are refused even offscreen: a transient CSS
|
||||
// reveal must not evade the checks surrounding native surface capture.
|
||||
if(/^(INPUT|TEXTAREA|SELECT)$/.test(e.tagName)&&
|
||||
(e.type==='password'||['name','id','autocomplete','aria-label'].some(a=>forbidden.test(e.getAttribute(a)||'')))){safe=false;return;}
|
||||
if(e.hasAttribute('data-sensitive')&&visible(e)){safe=false;return;}
|
||||
if(e.shadowRoot){walk(e.shadowRoot);if(!safe)return;}
|
||||
}
|
||||
}
|
||||
walk(document);
|
||||
const width=Math.round(innerWidth),height=Math.round(innerHeight);
|
||||
// Active media/animations are not represented by the DOM revision counter.
|
||||
if(state.watches.some(w=>(w.root.getAnimations?.()||[]).some(a=>a.playState==='running'))||
|
||||
[...document.querySelectorAll('video')].some(e=>visible(e)&&!e.paused))safe=false;
|
||||
return {safe,revision:[state.id,state.revision,width,height,scrollX,scrollY,devicePixelRatio,visualViewport?.scale||1].join(':'),width,height};
|
||||
}
|
||||
|
||||
async function probeVisualContext(context) {
|
||||
const contents=context.webContents,frame=contents.mainFrame;
|
||||
if(!frame||frame.isDestroyed()||frame.framesInSubtree.length!==1||contents.isLoading())throw Error('Managed visual export refused');
|
||||
await require('./page-clipboard-guard.cjs').guardedVisualStructure(contents);
|
||||
const result=await contents.executeJavaScriptInIsolatedWorld(1004,[{code:`(${visualDocumentProbe.toString()})(${JSON.stringify(context.url)})`}],false);
|
||||
if(contents.isDestroyed()||contents.mainFrame!==frame||frame.isDestroyed()||frame.framesInSubtree.length!==1)throw Error('Managed visual export refused');
|
||||
return result;
|
||||
}
|
||||
module.exports={probeVisualContext,visualDocumentProbe};
|
||||
Reference in new issue
Block a user