Files
safetynet/apps/browser/clipboard-guard.cjs
T
2026-10-13 10:34:48 +02:00

44 lines
3.4 KiB
JavaScript

'use strict';
const {randomUUID}=require('node:crypto');
function createClipboardGuard({getContext,stage,validate,commit,discard,readClipboard,writeClipboard,inspect,audit,timeoutMs=10000}){
let pending=null;
const current=c=>{const n=getContext(c.webContents);return n.ready&&n.tabId===c.tabId&&n.activation===c.activation&&n.policy===c.policy&&n.url===c.url&&n.navigationId===c.navigationId&&n.webContents===c.webContents&&n.webContents?.focusedFrame===n.webContents?.mainFrame&&Date.parse(n.policy?.expiresAt)>Date.now();};
async function run(operation,webContents){
const c=getContext(webContents),token=randomUUID();let text='',timer,expired=false;
const denied=async ruleId=>{await audit('clipboard.block',ruleId);return{completed:false,ruleId};};
const work=async()=>{try{
if(!['copy','cut','paste'].includes(operation)||!current(c))return denied('clipboard-context-denied');
const limit=Math.min(c.policy.security?.clipboard?.maxTextBytes||0,c.policy.dlp.maxBytes);
if(!limit)return denied('clipboard-policy-unavailable');
const snapshot=await stage({operation,token,url:c.url,limit,webContents:c.webContents});
if(!current(c)||expired)return denied('clipboard-context-changed');
if(operation==='paste')text=await readClipboard(limit);else text=snapshot.text;
if(!current(c)||expired)return denied('clipboard-context-changed');
if(typeof text!=='string'||Buffer.byteLength(text)>limit||text.includes('\0'))return denied('clipboard-size-limit');
const verdict=await inspect(Buffer.from(text),c.policy,operation);
if(!current(c)||expired)return denied('clipboard-context-changed');
if(verdict?.decision!=='allow')return denied(verdict?.ruleId||'clipboard-inspection-failed');
// Persist authorization before side effects; never audit raw clipboard text.
await audit(`clipboard.${operation}`);
if(!current(c)||expired)return denied('clipboard-context-changed');
const args={operation,token,url:c.url,text,limit,webContents:c.webContents};
if(operation==='cut'){
const validated=await validate(args);
if(validated?.completed!==true||!current(c)||expired)return denied('clipboard-target-changed');
await writeClipboard(text);
if(!current(c)||expired)return denied('clipboard-context-changed');
}
const result=await commit(args);
if(result?.completed!==true||!current(c)||expired)return denied('clipboard-target-changed');
if(operation==='copy'){await writeClipboard(text);if(!current(c)||expired)return denied('clipboard-context-changed');}
return {completed:true};
}finally{try{await discard?.({token,webContents:c.webContents});}catch{}}};
try{return await Promise.race([work(),new Promise(resolve=>{timer=setTimeout(()=>{expired=true;Promise.resolve(discard?.({token,webContents:c.webContents})).catch(()=>{});denied('clipboard-timeout').then(resolve,()=>resolve({completed:false,ruleId:'clipboard-timeout'}));},timeoutMs);})]);}
catch{return denied('clipboard-inspection-failed');}
finally{clearTimeout(timer);text='';}
}
function perform(operation,webContents){if(pending)return Promise.resolve({completed:false,ruleId:'clipboard-busy'});const task=run(operation,webContents);pending=task;task.finally(()=>{if(pending===task)pending=null;}).catch(()=>{});return task;}
return {perform,isPending:()=>!!pending};
}
module.exports={createClipboardGuard};