107 lines
8.2 KiB
JavaScript
107 lines
8.2 KiB
JavaScript
'use strict';
|
|
// This capability stays in the main process. Neither pages nor agents receive CDP access.
|
|
const owners=new WeakMap();
|
|
function protectClipboardPrimitives(){
|
|
const prototype=Document.prototype,existing=Object.getOwnPropertyDescriptor(prototype,'execCommand');
|
|
if(existing?.value?._managedClipboardGuard===true&&!existing.configurable)return;
|
|
const original=prototype.execCommand,apply=Reflect.apply,test=RegExp.prototype.test.bind(/^(copy|cut|paste)/i);
|
|
function managedClipboardCommand(command,...args){
|
|
if(typeof command!=='string'||test(command))return false;
|
|
return apply(original,this,[command,...args]);
|
|
}
|
|
Object.defineProperty(managedClipboardCommand,'_managedClipboardGuard',{value:true});
|
|
Object.defineProperty(prototype,'execCommand',{value:managedClipboardCommand,writable:false,configurable:false});
|
|
const SecurityException=DOMException;
|
|
const denyReplacement=()=>{throw new SecurityException('Managed document replacement is disabled','SecurityError');};
|
|
for(const name of ['open','write','writeln'])Object.defineProperty(prototype,name,{value:denyReplacement,writable:false,configurable:false});
|
|
const deny=event=>{event.preventDefault();event.stopImmediatePropagation();};
|
|
for(const name of ['copy','cut','paste','dragstart','dragover','drop'])window.addEventListener(name,deny,true);
|
|
const transferred=RegExp.prototype.test.bind(/^(insertFromPaste|insertFromDrop|deleteByCut)/);
|
|
window.addEventListener('beforeinput',event=>{if(transferred(event.inputType||''))deny(event);},true);
|
|
}
|
|
const source=`(${protectClipboardPrimitives.toString()})();`;
|
|
function deadline(operation){let timer;return Promise.race([operation,new Promise((_,reject)=>{timer=setTimeout(()=>reject(Error('Page security initialization timed out')),5000);})]).finally(()=>clearTimeout(timer));}
|
|
async function attachPageClipboardGuard(contents,onFailure){
|
|
const api=contents.debugger;
|
|
if(!api||api.isAttached()||owners.has(contents))throw Error('Page security initialization unavailable');
|
|
const owner={api,healthy:false,onFailure};
|
|
const detached=()=>{owners.delete(contents);owner.healthy=false;if(!contents.isDestroyed())onFailure();};
|
|
owner.detached=detached;
|
|
api.on('detach',detached);
|
|
try{
|
|
api.attach('1.3');
|
|
await deadline(api.sendCommand('Page.enable',{}));
|
|
const registration=await deadline(api.sendCommand('Page.addScriptToEvaluateOnNewDocument',{source}));
|
|
if(typeof registration?.identifier!=='string'||!registration.identifier||!api.isAttached())throw Error('Page security initialization failed');
|
|
owner.healthy=true;owners.set(contents,owner);
|
|
}catch{
|
|
api.removeListener('detach',detached);
|
|
if(api.isAttached())api.detach();
|
|
throw Error('Page security initialization failed');
|
|
}
|
|
}
|
|
function guardedScreenshot(contents,parameters){
|
|
const owner=owners.get(contents);
|
|
if(!owner)return null;
|
|
if(!owner.healthy||owner.api!==contents.debugger||!owner.api.isAttached())throw Error('Page security connection unavailable');
|
|
// Only the fixed screenshot command is shared. There is no general command dispatcher.
|
|
return owner.api.sendCommand('Page.captureScreenshot',parameters);
|
|
}
|
|
function disposePageClipboardGuard(contents){
|
|
const owner=owners.get(contents);if(!owner)return;
|
|
owner.releaseTransport?.();
|
|
owners.delete(contents);owner.healthy=false;owner.api.removeListener('detach',owner.detached);
|
|
if(owner.api.isAttached())owner.api.detach();
|
|
}
|
|
function createNativeTransportChannel(contents){
|
|
const owner=owners.get(contents),api=owner?.api;
|
|
if(!owner?.healthy||api!==contents.debugger||!api.isAttached()||owner.transportLease)throw Error('Private transport channel unavailable');
|
|
const lease=Symbol('native-transport'),subscriptions=new Set(),targets=new Set();let released=false;
|
|
owner.transportLease=lease;
|
|
const healthy=()=>!released&&owners.get(contents)===owner&&owner.healthy&&owner.transportLease===lease&&!contents.isDestroyed()&&api.isAttached();
|
|
const session=value=>value===undefined||typeof value==='string'&&value.length>0&&value.length<=128&&/^[a-zA-Z0-9._:-]+$/.test(value);
|
|
const command=(method,parameters,sessionId)=>{if(!healthy()||!session(sessionId))return Promise.reject(Error('Private transport channel unavailable'));return api.sendCommand(method,parameters,sessionId);};
|
|
const subscribe=(name,listener)=>{if(!healthy()||typeof listener!=='function')throw Error('Private transport subscription unavailable');api.on(name,listener);let removed=false;const remove=()=>{if(removed)return;removed=true;api.removeListener(name,listener);subscriptions.delete(remove);};subscriptions.add(remove);return remove;};
|
|
const messages=new Set(['Target.attachedToTarget','Target.detachedFromTarget','Network.webTransportCreated','Network.webTransportConnectionEstablished','Network.webTransportClosed']);
|
|
function release(){
|
|
if(released)return;released=true;for(const remove of [...subscriptions])remove();
|
|
if(owner.transportLease===lease){owner.transportLease=null;owner.releaseTransport=null;}
|
|
// Release private observation without detaching the clipboard security owner.
|
|
// Fresh workers may still be waiting for startup; resume before dropping targets.
|
|
if(api.isAttached()){
|
|
const send=(method,parameters,id)=>{try{return Promise.resolve(api.sendCommand(method,parameters,id));}catch{return Promise.reject(Error('Private transport cleanup unavailable'));}};
|
|
const pending=[];for(const id of targets)pending.push(send('Runtime.runIfWaitingForDebugger',{},id));
|
|
pending.push(send('Target.setAutoAttach',{autoAttach:false,waitForDebuggerOnStart:false,flatten:true}));
|
|
void Promise.allSettled(pending).then(results=>{if(results.some(result=>result.status==='rejected')&&owners.get(contents)===owner&&owner.healthy&&!contents.isDestroyed())owner.onFailure();});
|
|
}
|
|
targets.clear();
|
|
}
|
|
owner.releaseTransport=release;
|
|
return Object.freeze({isHealthy:healthy,
|
|
enableNetwork:sessionId=>command('Network.enable',{maxPostDataSize:0},sessionId),
|
|
autoAttachTargets:sessionId=>command('Target.setAutoAttach',{autoAttach:true,waitForDebuggerOnStart:true,flatten:true},sessionId),
|
|
resumeTarget:sessionId=>sessionId===undefined?Promise.reject(Error('Private transport target required')):command('Runtime.runIfWaitingForDebugger',{},sessionId),
|
|
subscribeMessage:listener=>subscribe('message',(event,method,params,sessionId)=>{if(!messages.has(method))return;if(method==='Target.attachedToTarget'&&session(params?.sessionId)&¶ms.sessionId!==undefined)targets.add(params.sessionId);if(method==='Target.detachedFromTarget')targets.delete(params?.sessionId);listener(event,method,params,sessionId);}),
|
|
subscribeDetach:listener=>subscribe('detach',()=>listener()),release});
|
|
}
|
|
async function guardedVisualStructure(contents){
|
|
const owner=owners.get(contents);
|
|
if(!owner?.healthy||!owner.api.isAttached()||contents.isDestroyed())throw Error('Managed visual export refused');
|
|
// Native DOM data detects closed/declarative roots that isolated DOM queries
|
|
// cannot see. It is consumed privately and never logged or returned to agents.
|
|
const result=await deadline(owner.api.sendCommand('DOM.getDocument',{depth:-1,pierce:true}));
|
|
if(owners.get(contents)!==owner||!owner.healthy||!owner.api.isAttached())throw Error('Managed visual export refused');
|
|
const queue=[result?.root];let nodes=0,characters=0;
|
|
while(queue.length){
|
|
const node=queue.pop();if(!node||++nodes>20000)throw Error('Managed visual export refused');
|
|
for(const text of [node.nodeName,node.nodeValue,...(node.attributes||[])]){if(typeof text!=='string'||(characters+=text.length)>1024*1024)throw Error('Managed visual export refused');}
|
|
const shadows=node.shadowRoots||[],children=node.children||[];
|
|
if(!Array.isArray(shadows)||!Array.isArray(children)||nodes+queue.length+shadows.length+children.length>20000)throw Error('Managed visual export refused');
|
|
for(const root of shadows){if(root.shadowRootType==='closed')throw Error('Managed visual export refused');queue.push(root);}
|
|
if(node.contentDocument)throw Error('Managed visual export refused');
|
|
queue.push(...children);
|
|
}
|
|
return {safe:true,nodes};
|
|
}
|
|
module.exports={attachPageClipboardGuard,guardedScreenshot,guardedVisualStructure,disposePageClipboardGuard,createNativeTransportChannel};
|