From 02e99f76233bfb279af40247324c2e3478a4fda7 Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Sun, 12 Jul 2026 11:27:57 -0700 Subject: [PATCH] Add design spec: site builder security & data-loss hardening Covers Critical (stored-XSS escaping cluster) + High (copy/paste id reuse, header/footer save corruption, AI-boundary validation) findings from the 2026-07-12 audit. All claims verified against code. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...-site-builder-security-hardening-design.md | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-12-site-builder-security-hardening-design.md diff --git a/docs/superpowers/specs/2026-07-12-site-builder-security-hardening-design.md b/docs/superpowers/specs/2026-07-12-site-builder-security-hardening-design.md new file mode 100644 index 0000000..b197bc4 --- /dev/null +++ b/docs/superpowers/specs/2026-07-12-site-builder-security-hardening-design.md @@ -0,0 +1,128 @@ +# Site Builder — security & data-loss hardening (2026-07-12) + +Fixes the **Critical** (stored-XSS / HTML-breakout) and **High** (state-corruption +and save data-loss) findings from the 2026-07-12 Fable audit of the Craft.js site +builder (`/workspace/site-builder/craft/`). Medium/Low findings are deferred to the +audit backlog (in memory), not this spec. Sequenced **before** the asset-picker +work per user decision. Every claim below was verified against the code before +writing this spec. + +## 1 — Escaping cluster (Critical, do first: closes the most with one change) + +**Verified root cause:** `escapeHtml` exists once (`utils/html-export.ts:201`) but is +**not exported** and no component uses it; there are **27** local `esc`/`escapeHtml` +definitions across `src/`, in three incompatible variants (some omit `&`, some omit +quotes entirely, none escape `'`), and several land in attribute or JS-string +contexts where HTML-entity escaping is wrong anyway. + +### 1a — Shared escaping/URL util +New `craft/src/utils/escape.ts` (exported, single source of truth): + +```ts +export function escapeHtml(s: string): string; // & < > " — text/content context +export function escapeAttr(s: string): string; // escapeHtml + ' — attribute context +export function safeUrl(s: string): string; // returns '' (or '#') for javascript:/data:text/html/vbscript: schemes +``` + +- Move the correct body from `html-export.ts` into `escape.ts`; `html-export.ts` + imports it. Delete all 27 local copies and import from `escape.ts`. +- `safeUrl` wraps every exported `href`/`src`/`action`/`url()` value: `ButtonLink`, + `Icon`, `SocialLinks`, `Logo`, `Menu`, `Navbar`, `_cta-helpers`, `ContentSlider`, + `FeaturesGrid`, `PricingTable`, `ImageBlock`, `VideoBlock`, `Gallery`, background + images. Attribute values use `escapeAttr`; text nodes use `escapeHtml`. + +### 1b — JS-string / raw-HTML contexts (entity escaping is NOT enough here) +- **HtmlBlock** (`basic/HtmlBlock.tsx:144`): `toHtml` returns `props.code` raw while + the render sanitizes via the file's own `purifyHtml`. Fix: `return { html: + purifyHtml(props.code || '') }`. (DOMPurify already a dep.) +- **Countdown** (`sections/Countdown.tsx:298`): `new Date("${targetDate}")` inside an + inline `