Sitesmith capture: CSS judged via CSSOM (no regex), single URL gate (no % / dot segments / backslash), all resources inlined through a bounded no-credentials fetch, CSP img-src data: only; bypass-class proof tests (unit + Firefox/Chromium)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c4f45541a3
commit
f0b3dd1dba
7 files changed
+523
-93
No files matched your search
@@ -1,13 +1,13 @@
|
||||
// Bundled by scripts/e2e-capture-security.mjs and run in real Firefox + Chromium.
|
||||
import { captureHtml } from '../src/utils/self-review-capture';
|
||||
import { captureCsp } from '../src/utils/sanitize-capture-html';
|
||||
import { CAPTURE_CSP } from '../src/utils/sanitize-capture-html';
|
||||
|
||||
(window as any).captureHtml = captureHtml;
|
||||
/** Defence-in-depth probe: the capture frame WITHOUT the sanitiser (sandbox + CSP only). */
|
||||
(window as any).rawFrame = (html: string) => new Promise<void>((resolve) => {
|
||||
const f = document.createElement('iframe');
|
||||
f.setAttribute('sandbox', 'allow-same-origin');
|
||||
const CSP = captureCsp(location.origin);
|
||||
const CSP = CAPTURE_CSP;
|
||||
f.setAttribute('csp', CSP);
|
||||
f.style.cssText = 'position:fixed;left:-9999px;width:800px;height:600px';
|
||||
f.addEventListener('load', () => setTimeout(resolve, 800), { once: true });
|
||||
|
||||
@@ -36,6 +36,17 @@ const EVIL = `<!doctype html><html><head>
|
||||
<object data="/beacon?object"></object><embed src="/beacon?embed">
|
||||
<div style="background:url(/beacon?cssurl)">x</div><video src="/beacon?video"></video>
|
||||
<input autofocus onfocus="${PWN}"><details open ontoggle="${PWN}"><summary>s</summary></details>
|
||||
<style>
|
||||
.b1{background:u\\72l(/beacon?b-escape)} .b2{background-image:image-set("/beacon?b-imageset" 1x)} .b3{background:url("x)/beacon?b-paren")}
|
||||
.b4{background:ur/**/l(/beacon?b-comment)} :root{--u:url(/beacon?b-var)} .b5{background:var(--u)}
|
||||
@font-face{font-family:bx;src:url(/beacon?b-fontface)} .b6{font-family:bx}
|
||||
@font-face{font-family:by;src:url(/api/site-builder.php?action=delete_asset)} .b7{font-family:by}
|
||||
.b8{background:url(assets/%2e%2e/beacon?b-pct)} .b9{background:url(assets/%252e%252e/beacon?b-pct2)} .b10{background:url(assets/..%2fbeacon?b-pct3)}
|
||||
.b11{background:url(javascript:1)} .b12{background:url(blob:http://x/y)} .b13{background:url("data:text/html,<b>1</b>")} .b14{-moz-binding:url(/beacon?b-moz)}
|
||||
@keyframes bk{from{background:url(/beacon?b-kf)}to{color:red}} @media (min-width:1px){.b15{background:url(/beacon?b-media)}}
|
||||
</style>
|
||||
<div class="b1 b2 b3 b4 b5 b6 b7 b8 b9 b10 b11 b12 b13 b14 b15" style="background:u\\72l(/beacon?i-escape);background-image:image-set('/beacon?i-set' 1x)">css bypass probes</div>
|
||||
<img src="assets/%2e%2e/beacon?p1"><img src="assets/%252e%252e/beacon?p2"><img src="assets/..%2fbeacon?p3"><img src="assets/../beacon?p4"><img src="//localhost/beacon?p5"><img src="data:text/html,x"><img src="blob:http://x/y"><img src="assets\\..\\beacon?p6">
|
||||
<p style="color:#444">Visible paragraph so the capture has real content ${'lorem ipsum '.repeat(40)}</p>
|
||||
</body></html>`;
|
||||
const BENIGN = `<!doctype html><html><head><style>body{margin:0;font:20px sans-serif}.h{background:linear-gradient(135deg,#0f172a,#3b82f6);color:#fff;padding:40px}</style></head>
|
||||
@@ -73,7 +84,7 @@ async function run(name, launcher, exe) {
|
||||
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: no request to the panel origin /beacon', beacons.length === 0, JSON.stringify(beacons));
|
||||
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));
|
||||
@@ -86,7 +97,7 @@ async function run(name, launcher, exe) {
|
||||
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: no script-initiated requests (js/img-js beacons)', !beacons.some((b) => /\?(js|img-js|refresh|import|iframe|object|embed|video)$/.test(b)), JSON.stringify(beacons));
|
||||
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()));
|
||||
|
||||
|
||||
Reference in new issue
Block a user