#!/usr/bin/env node /** * Real-browser proof (Firefox + Chromium) that the self-review capture cannot run or leak hostile page * markup. A malicious fixture goes through the WHOLE capture path; sentinels: window.__pwned in the * parent, any request to /beacon or evil.example, parent cookie/localStorage reachability, frame * navigation. A second probe feeds the SAME hostile page to the frame WITHOUT the sanitiser to show the * sandbox + CSP alone also hold. Benign pages must still capture. * Run: node scripts/e2e-capture-security.mjs (CHROMIUM_PATH / FIREFOX_PATH override the binaries) */ import { build } from 'esbuild'; import { chromium, firefox } from '@playwright/test'; import http from 'node:http'; import { fileURLToPath } from 'node:url'; import path from 'node:path'; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const out = await build({ entryPoints: [path.join(root, 'scripts/e2e-capture-security-entry.ts')], bundle: true, write: false, format: 'iife', platform: 'browser', logLevel: 'error', define: { 'process.env.NODE_ENV': '"production"' }, }); const js = out.outputFiles[0].text; const PWN = "(function(){try{parent.__pwned=1;top.__pwned=1}catch(e){}try{fetch('/beacon?js')}catch(e){}try{top.postMessage('pwned','*')}catch(e){}try{new Image().src='/beacon?img-js'}catch(e){}})()"; const EVIL = `

Hostile but readable

js link
x
s
css bypass probes

Visible paragraph so the capture has real content ${'lorem ipsum '.repeat(40)}

`; const BENIGN = `

Benign page

${'Hello world. '.repeat(30)}

`; const beacons = []; const srv = http.createServer((q, r) => { const p = q.url.split('?')[0]; if (p === '/') { r.setHeader('content-type', 'text/html'); return r.end('hosthost'); } if (p === '/beacon') { beacons.push(q.url); r.statusCode = 204; return r.end(); } r.statusCode = 404; r.end('nf'); }).listen(0); const port = srv.address().port; let failed = 0; const check = (name, label, ok, extra = '') => { if (!ok) failed++; console.log(`[${name}] ${ok ? 'PASS' : 'FAIL'} ${label}${extra ? ' ' + extra : ''}`); }; async function run(name, launcher, exe) { const browser = await launcher.launch({ executablePath: exe || undefined, ...(name === 'chromium' ? { args: ['--no-sandbox'] } : {}) }); const ctx = await browser.newContext(); const page = await ctx.newPage(); const evilReq = []; await ctx.route(/evil\.example/, (route) => { evilReq.push(route.request().url()); route.abort(); }); // Only requests that actually reach the network count (Playwright also reports CSP-blocked ones as 'request'). let messages = 0; await page.exposeFunction('__onMsg', () => { messages++; }); await page.goto(`http://localhost:${port}/`); await page.evaluate(() => { document.cookie = 'sess=SECRET; path=/'; localStorage.setItem('csrf', 'SECRET'); window.addEventListener('message', (e) => { if (e.data === 'pwned') window.__onMsg(); }); }); await page.addScriptTag({ content: js }); // 1) hostile page through the whole capture path beacons.length = 0; const res = await page.evaluate(async (h) => { const r = await window.captureHtml(h, { settleMs: 300 }); return r.ok ? { ok: true, size: r.blob.size } : r; }, EVIL); await page.waitForTimeout(500); const pwned = await page.evaluate(() => window.__pwned); check(name, 'hostile page: parent sentinel window.__pwned untouched', pwned === undefined, `(${pwned})`); check(name, 'hostile page: no postMessage to the editor', messages === 0, `(${messages})`); check(name, 'hostile page: ZERO requests to the panel origin /beacon (every CSS/URL/encoding bypass class)', beacons.length === 0, JSON.stringify(beacons)); check(name, 'hostile page: no request to evil.example', evilReq.length === 0, JSON.stringify(evilReq)); check(name, 'hostile page: capture result is safe (ok, or refused) and frame removed', (res.ok || typeof res.reason === 'string') && (await page.evaluate(() => document.querySelectorAll('iframe').length)) === 0, JSON.stringify(res)); check(name, 'hostile page: sanitised content still captured (readable page)', res.ok === true && res.size > 3000, JSON.stringify(res)); // 2) defence in depth: same hostile page, NO sanitiser, only sandbox + CSP beacons.length = 0; evilReq.length = 0; messages = 0; await page.evaluate((h) => window.rawFrame(h), EVIL); await page.waitForTimeout(500); const pwned2 = await page.evaluate(() => window.__pwned); check(name, 'unsanitised probe: sandbox+CSP alone stop script execution (__pwned)', pwned2 === undefined, `(${pwned2})`); check(name, 'unsanitised probe: no postMessage', messages === 0); // Same-origin passive GETs (img/css url) are allowed by CSP 'self'; that is why the sanitiser also restricts them. Script-initiated ones must never occur. check(name, 'unsanitised probe: CSP alone (img-src data:) lets no request reach the panel origin', beacons.length === 0, JSON.stringify(beacons)); check(name, 'unsanitised probe: no evil.example requests', evilReq.length === 0, JSON.stringify(evilReq)); await page.evaluate(() => document.querySelectorAll('iframe').forEach((f) => f.remove())); // 3) benign page still captures const ok = await page.evaluate(async (h) => { const r = await window.captureHtml(h, { settleMs: 300 }); return r.ok ? { ok: true, size: r.blob.size, w: r.width, h: r.height } : r; }, BENIGN); check(name, 'benign page captures', ok.ok === true && ok.size > 3000, JSON.stringify(ok)); await browser.close(); } await run('chromium', chromium, process.env.CHROMIUM_PATH); await run('firefox', firefox, process.env.FIREFOX_PATH); srv.close(); process.exit(failed ? 1 : 0);