fix(security): hardened CSP on /deco/render (reflected XSS execution vector) - #509
Open
0xcucumbersalad wants to merge 1 commit into
Open
fix(security): hardened CSP on /deco/render (reflected XSS execution vector)#5090xcucumbersalad wants to merge 1 commit into
0xcucumbersalad wants to merge 1 commit into
Conversation
…ed XSS)
`/deco/render` renders any registered section with fully caller-controlled
props as text/html, unauthenticated. A rich-text prop reaching an HTML sink
(`dangerouslySetInnerHTML`) is reflected XSS delivered via
`GET /deco/render?resolveChain=...&props=...`. The response carried no CSP, and
deco's enforced CSP still ships `script-src 'unsafe-inline'`, so an injected
`<img onerror>` executes.
Add an execution-layer mitigation on the render response only (leaves the
`/deco/invoke` runtime RPC untouched — it is not preview-only and returns JSON):
- `buildRenderCSP({ nonce, adminOrigins })` in `blocks/sdk/csp`: a locked-down
policy — `default-src 'none'`, `script-src 'nonce-<per-response>'` (no
`unsafe-inline`, so inline `onerror`/`onload` attributes and un-nonced
`<script>` are blocked), `base-uri`/`form-action` `'none'`, admin-only
`frame-ancestors` (also closes clickjacking). Non-script directives stay
permissive so the preview still paints.
- `generateCSPNonce()`: 128-bit Web Crypto nonce, fresh per response.
- `htmlShell` threads a `nonce` onto the framework's inline preview script so
`LIVE_CONTROLS_SCRIPT` survives the nonce policy.
- `handleRender` generates one nonce per response and stamps the CSP +
`X-Content-Type-Options: nosniff` on every path (section, page, unknown,
error) via a shared `htmlResponse` helper.
- `getAdminOrigins()` exported from `cors` for the real studio origin set.
Defense-in-depth: the site-level fix is still to `sanitizeHtml` each rich-text
sink. This closes the execution vector framework-wide regardless.
Tests: csp.test.ts (builder + nonce), render.csp.test.ts (payload reflected but
inert, nonce match, per-response freshness, error path). 21 pass; existing
resolvePreview.test.ts unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
/deco/render(handleRender) renders any registered section with fully caller-controlled props astext/html, unauthenticated — wrapped only inwithCors/withTracing. A caller-controlled rich-text prop that reaches an HTML sink (dangerouslySetInnerHTML) is reflected XSS, delivered via:The render response carried no CSP, and deco's enforced CSP still ships
script-src 'unsafe-inline'(the strict policy is report-only), so an injected<img src=x onerror=alert(document.domain)>executes in the victim's browser.This was surfaced auditing a downstream storefront: 7 confirmed raw section sinks (
marquees[].text, title-box rich text, …) reachable through/deco/renderwith no sanitizer.Scope decision
/deco/renderonly./deco/invokeis deliberately not touched — it is the storefronts' runtime RPC (lazy sections, cart actions, search) called from public browsers and returns JSON, not HTML. Blanket-gating it would break every live site; it is not the XSS delivery route./deco/rendercross-origin with no credential today, so a prod auth gate would break admin preview everywhere pending cross-repo coordination. This PR ships the execution-layer mitigation that needs zero coordination and closes the vector regardless of which section sink exists.What changed
blocks/sdk/csp.ts—buildRenderCSP({ nonce, adminOrigins }):default-src 'none';script-src 'nonce-<per-response>'(nounsafe-inline→ inlineonerror/onloadattributes and un-nonced<script>are blocked = the payload is neutralized);base-uri/form-action 'none'; admin-onlyframe-ancestors(also closes clickjacking). Non-script directives (img/style/font) stay permissive so the preview still paints. PlusgenerateCSPNonce()(128-bit Web Crypto, fresh per response).blocks-admin/sdk/htmlShell.ts— threads anonceonto the framework's inline preview<script>soLIVE_CONTROLS_SCRIPTsurvives the nonce policy.blocks-admin/admin/render.ts— one nonce per response; CSP +X-Content-Type-Options: nosniffstamped on every path (section, page, unknown, error) via a sharedhtmlResponsehelper.blocks-admin/admin/cors.ts— exportsgetAdminOrigins()for the real studio origin set.withCorscopies existing response headers before adding CORS, so the CSP survives both the TanStack and Next.js adapters.Tests
blocks/sdk/csp.test.ts— builder directives + nonce freshness.blocks-admin/admin/render.csp.test.ts— payload is reflected (proves the sink) but the response carries the neutralizing policy; framework script nonce matches the CSP; per-response nonce freshness; error path also stamped.resolvePreview.test.tsunchanged (no regression).Not in scope / follow-ups
JSON.stringifybreakout viahtmlSafe*) — orthogonal flavor.sanitizeHtmleach rich-text sink.<script>(carousels, etc.) is also blocked in preview — acceptable trade for preview; noted for follow-up if any admin-preview interactivity regresses.🤖 Generated with Claude Code
Summary by cubic
Hardens the CSP on
/deco/renderso reflected XSS payloads from caller-controlled section props can no longer execute. The endpoint previously returned HTML with no CSP, letting injected inline handlers like<img onerror=...>run in the victim's browser; every response now carries a nonce-based CSP that blocks them.Details
script-src(nounsafe-inline) withX-Content-Type-Options: nosniffon every path, including errors, and tags the framework's own preview script with the same nonce so it keeps working.frame-ancestorsto admin origins, which also closes clickjacking./deco/invokeuntouched since it returns JSON and gating it would break live storefronts; no auth gate was added to avoid breaking the studio's cross-origin iframe.sanitizeHtmlon rich-text sinks remains the site-level follow-up.Written for commit ec4c69b. Summary will update on new commits.