#!/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('host host'); }
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);