diff --git a/craft/package-lock.json b/craft/package-lock.json
index 07fd023..f7bea9a 100644
--- a/craft/package-lock.json
+++ b/craft/package-lock.json
@@ -20,6 +20,7 @@
"@craftjs/core": "^0.2.10",
"@craftjs/layers": "^0.2.7",
"dompurify": "^3.4.5",
+ "html-to-image": "1.11.13",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
@@ -2349,6 +2350,12 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
+ "node_modules/html-to-image": {
+ "version": "1.11.13",
+ "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz",
+ "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==",
+ "license": "MIT"
+ },
"node_modules/immer": {
"version": "9.0.21",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
diff --git a/craft/package.json b/craft/package.json
index bb63423..1e910b9 100644
--- a/craft/package.json
+++ b/craft/package.json
@@ -16,6 +16,7 @@
"test:browser:mobile": "node scripts/e2e-mobile-parity.mjs",
"test:browser:colorpick": "node scripts/e2e-color-pick.mjs",
"test:browser:colors": "node scripts/e2e-color-system.mjs",
+ "test:browser:sitesmith": "node scripts/e2e-sitesmith.mjs",
"test:unit:watch": "vitest"
},
"dependencies": {
@@ -31,6 +32,7 @@
"@craftjs/core": "^0.2.10",
"@craftjs/layers": "^0.2.7",
"dompurify": "^3.4.5",
+ "html-to-image": "1.11.13",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
diff --git a/craft/scripts/e2e-capture-security-entry.ts b/craft/scripts/e2e-capture-security-entry.ts
new file mode 100644
index 0000000..5ad3533
--- /dev/null
+++ b/craft/scripts/e2e-capture-security-entry.ts
@@ -0,0 +1,16 @@
+// Bundled by scripts/e2e-capture-security.mjs and run in real Firefox + Chromium.
+import { captureHtml } from '../src/utils/self-review-capture';
+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((resolve) => {
+ const f = document.createElement('iframe');
+ f.setAttribute('sandbox', 'allow-same-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 });
+ f.srcdoc = html.replace('', ``);
+ document.body.appendChild(f);
+});
diff --git a/craft/scripts/e2e-capture-security.mjs b/craft/scripts/e2e-capture-security.mjs
new file mode 100644
index 0000000..1f54196
--- /dev/null
+++ b/craft/scripts/e2e-capture-security.mjs
@@ -0,0 +1,112 @@
+#!/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
+
+
">',
+ '