62 lines
3.7 KiB
JavaScript
62 lines
3.7 KiB
JavaScript
'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};
|