2026-10-13 10:33:31 +02:00
import test from 'node:test' ;
import assert from 'node:assert/strict' ;
import { createRequire } from 'node:module' ;
import vm from 'node:vm' ;
import { defaultPolicy } from '../packages/core/policy.mjs' ;
const require = createRequire ( import . meta . url ),{ createClipboardGuard } = require ( '../apps/browser/clipboard-guard.cjs' ),{ clipboardScript } = require ( '../apps/browser/clipboard-dom.cjs' );
const deferred = ()=>{ let resolve ; const promise = new Promise ( r => resolve = r ); return { promise , resolve };};
function guardFixture ( inspect ){
const frame = {}, webContents = { mainFrame : frame , focusedFrame : frame }; let context = { ready : true , policy : defaultPolicy (), url : 'https://company.example/login' , navigationId : 1 , webContents }; const writes = [], commits = [], discards = [], audit = [];
const guard = createClipboardGuard ({ getContext : ()=> context , stage : async ()=>({ text : 'public selected text' }), validate : async ()=>({ completed : true }), commit : async args =>{ commits . push ( args ); return { completed : true };}, discard : async args => discards . push ( args ), readClipboard : ()=> 'public pasted text' , writeClipboard : text => writes . push ( text ), inspect , audit : async (... args )=> audit . push ( args ), timeoutMs : 25 });
return { guard , webContents , writes , commits , discards , audit , context : ()=> context , setContext : c =>{ context = c ;}};
}
test ( 'independent clipboard review: changed policy, navigation or readiness prevents release after scanning' , async ()=>{
for ( const changed of [ 'policy' , 'url' , 'navigationId' , 'ready' ]){ const pending = deferred (), started = deferred (), f = guardFixture ( async ()=>{ started . resolve (); return pending . promise ;}); const operation = f . guard . perform ( 'copy' , f . webContents ); await started . promise ; const context = {... f . context ()}; context [ changed ] = changed === 'policy' ? {... context . policy , version : 2 } : changed === 'url' ? 'https://other.example/' : changed === 'navigationId' ? 2 : false ; f . setContext ( context ); pending . resolve ({ decision : 'allow' }); assert . equal (( await operation ). completed , false , changed ); assert . equal ( f . writes . length , 0 ); assert . equal ( f . commits . length , 0 ); assert . equal ( f . discards . length , 1 ); assert . equal ( JSON . stringify ( f . audit ). includes ( 'public selected text' ), false );}
});
test ( 'independent clipboard review: timeout does not authorize delayed inspection completion' , async ()=>{
const pending = deferred (), started = deferred (), f = guardFixture ( async ()=>{ started . resolve (); return pending . promise ;}); const operation = f . guard . perform ( 'paste' , f . webContents ); await started . promise ; const result = await operation ; assert . equal ( result . ruleId , 'clipboard-timeout' ); pending . resolve ({ decision : 'allow' }); await new Promise ( r => setImmediate ( r )); assert . equal ( f . writes . length , 0 ); assert . equal ( f . commits . length , 0 ); assert . ok ( f . discards . length >= 1 );
});
test ( 'switching away and back during scan invalidates clipboard authorization' , async ()=>{
const pending = deferred (), started = deferred (), f = guardFixture ( async ()=>{ started . resolve (); return pending . promise ;});
f . setContext ({... f . context (), tabId : 'tab-a' , activation : 1 });
const operation = f . guard . perform ( 'copy' , f . webContents ); await started . promise ;
f . setContext ({... f . context (), tabId : 'tab-a' , activation : 3 });
pending . resolve ({ decision : 'allow' }); assert . equal (( await operation ). completed , false );
assert . equal ( f . writes . length , 0 ); assert . equal ( f . commits . length , 0 );
});
test ( 'independent clipboard review: focused nested frame cannot request a main-frame copy' , async ()=>{
const f = guardFixture ( async ()=>({ decision : 'allow' })); f . webContents . focusedFrame = {}; assert . equal (( await f . guard . perform ( 'copy' , f . webContents )). completed , false ); assert . equal ( f . writes . length , 0 ); assert . equal ( f . commits . length , 0 );
});
test ( 'independent clipboard review: failed native clipboard write cannot delete a cut selection' , async ()=>{
const frame = {}, webContents = { mainFrame : frame , focusedFrame : frame }, context = { ready : true , policy : defaultPolicy (), url : 'https://company.example/' , navigationId : 1 , webContents }; let mutations = 0 , discarded = 0 ;
const guard = createClipboardGuard ({ getContext : ()=> context , stage : async ()=>({ text : 'selected public text' }), validate : async ()=>({ completed : true }), commit : async ()=>{ mutations ++ ; return { completed : true };}, discard : async ()=>{ discarded ++ ;}, readClipboard : ()=> '' , writeClipboard : ()=>{ throw Error ( 'Native write failed' );}, inspect : async ()=>({ decision : 'allow' }), audit : async ()=>{}});
assert . equal (( await guard . perform ( 'cut' , webContents )). completed , false ); assert . equal ( mutations , 0 ); assert . equal ( discarded , 1 );
});
function domFixture (){
const matches = ( node , selector )=>{ selector = selector . trim (); if ( selector . startsWith ( '[' )){ const match = /^\[([^=\]]+)(?:="([^"]+)")?\]$/ . exec ( selector ); return !! match && Object . hasOwn ( node . attributes , match [ 1 ]) && ( match [ 2 ] === undefined || node . attributes [ match [ 1 ]] === match [ 2 ]);} return node . tagName . toLowerCase () === selector ;};
const element = ( tag , parent = null , attributes = {})=>{ const e = { nodeType : 1 , tagName : tag . toUpperCase (), parentElement : parent , attributes , children : [], isConnected : true , type : attributes . type || '' , style : { display : 'block' , visibility : 'visible' , opacity : '1' }, getAttribute : name => e . attributes [ name ] ?? null , getClientRects : ()=>[{ width : 100 , height : 16 }], closest : selector =>{ for ( let p = e ; p ; p = p . parentElement ) if ( selector . split ( ',' ). some ( s => matches ( p , s ))) return p ; return null ;}, querySelector : selector => descendants ( e ). find ( n => selector . split ( ',' ). some ( s => matches ( n , s ))) || null }; parent ? . children . push ( e ); return e ;};
const descendants = e => e . children . flatMap ( c =>[ c ,... descendants ( c )]), body = element ( 'body' ), paragraph = element ( 'p' , body ), child = element ( 'span' , paragraph ), range = { commonAncestorContainer : paragraph , startContainer : paragraph , endContainer : paragraph , startOffset : 0 , endOffset : 1 , toString : ()=> 'public text with selected hidden content' , cloneRange (){ return {... this };}, intersectsNode : ()=> true }, selection = { rangeCount : 1 , isCollapsed : false , getRangeAt : ()=> range };
const document = { body , activeElement : body , createTreeWalker : root =>{ const nodes = descendants ( root ); let index = 0 ; return { nextNode : ()=> nodes [ index ++ ] || null };}}, window = {}; window . top = window ;
const sandbox = { window , document , location : { href : 'https://company.example/' }, NodeFilter : { SHOW_ELEMENT : 1 }, getSelection : ()=> selection , getComputedStyle : e => e . style , Event : class {}}; vm . createContext ( sandbox );
const execute = ( mode = 'stage' , operation = 'copy' )=> vm . runInContext ( clipboardScript ( mode ,{ url : sandbox . location . href , token : 'trusted-token' , operation , text : 'public replacement' , limit : 1024 }), sandbox );
return { sandbox , document , paragraph , child , element , range , execute };
}
test ( 'independent clipboard review: selected hidden, inert, script and ancestor-transparent content is refused' ,()=>{
for ( const type of [ 'hidden' , 'inert' , 'aria-hidden' , 'script' , 'transparent' ]){ const f = domFixture (); if ( type === 'script' ) f . child . tagName = 'SCRIPT' ; else if ( type === 'transparent' ) f . paragraph . style . opacity = '0' ; else f . child . attributes [ type ] = type === 'aria-hidden' ? 'true' : '' ; assert . throws (()=> f . execute (), undefined , type ); assert . equal ( f . sandbox . __safetynetClipboardSnapshot , undefined );}
const publicRange = domFixture (); assert . equal ( publicRange . execute (). text , 'public text with selected hidden content' ); publicRange . child . attributes . hidden = '' ; assert . throws (()=> publicRange . execute ( 'commit' )); assert . equal ( publicRange . sandbox . __safetynetClipboardSnapshot , undefined );
});
test ( 'independent clipboard review: password fields and parent-hidden text fields never reach the scanner' ,()=>{
for ( const type of [ 'password' , 'secret-name' , 'transparent' ]){ const f = domFixture (), input = f . element ( 'input' , f . paragraph ,{ type : type === 'password' ? 'password' : 'text' ,...( type === 'secret-name' ? { name : 'api_token' } : {})}); Object . assign ( input ,{ value : 'private field secret' , selectionStart : 0 , selectionEnd : 20 , disabled : false , readOnly : false }); f . document . activeElement = input ; if ( type === 'transparent' ) f . paragraph . style . opacity = '0' ; assert . throws (()=> f . execute ()); assert . equal ( f . sandbox . __safetynetClipboardSnapshot , undefined );}
});
2026-10-13 18:04:02 +02:00
test ( 'clipboard completion metadata follows actual commit and native write, never exposes content' , async ()=>{
const frame = {}, wc = { mainFrame : frame , focusedFrame : frame }, steps = [], events = [];
let c = { ready : true , tabId : '71911cdc-a390-4e73-a604-d98d05fd52a5' , activation : 1 , policy : defaultPolicy (), url : 'https://company.example/' , navigationId : 1 , webContents : wc };
const options = { getContext : ()=> c , stage : async ()=>({ text : 'safe text' , target : { tag : 'input' , x : 10 , y : 20 , text : 'SECRET' , selector : '#secret' }}), validate : async ()=>({ completed : true }), commit : async ()=>{ steps . push ( 'commit' ); return { completed : true };}, readClipboard : async ()=> 'safe paste' , writeClipboard : async ()=> steps . push ( 'write' ), inspect : async ()=>({ decision : 'allow' }), audit : async ( kind , rule , facts )=>{ steps . push ( 'audit' ); assert . equal ( JSON . stringify ( facts ). includes ( 'safe text' ), false );}, onCompleted : async metadata =>{ steps . push ( 'completed' ); events . push ( metadata );}};
const guard = createClipboardGuard ( options ); assert . equal (( await guard . perform ( 'copy' , wc )). completed , true );
assert . deepEqual ( steps ,[ 'audit' , 'commit' , 'write' , 'completed' ]); assert . deepEqual ( events ,[{ action : 'copy' , sessionId : c . tabId , pageUrl : c . url , outcome : 'completed' , sensitiveField : false , target : { tag : 'input' , x : 10 , y : 20 }, bytes : 9 }]);
events . length = 0 ; options . inspect = async ()=>({ decision : 'block' , ruleId : 'blocked' }); assert . equal (( await createClipboardGuard ( options ). perform ( 'paste' , wc )). completed , false ); assert . equal ( events . length , 0 );
options . inspect = async ()=>({ decision : 'allow' }); options . writeClipboard = async ()=>{ c = {... c , navigationId : c . navigationId + 1 };}; assert . equal (( await createClipboardGuard ( options ). perform ( 'copy' , wc )). completed , false ); assert . equal ( events . length , 0 );
});