From ec4c69baf037f14f6a4f828efe106f639a8bf52d Mon Sep 17 00:00:00 2001 From: Jaynel Patiarba Date: Fri, 28 Aug 2026 00:49:38 +0800 Subject: [PATCH 1/2] fix(security): hardened CSP on /deco/render preview response (reflected XSS) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `/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 `` 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-'` (no `unsafe-inline`, so inline `onerror`/`onload` attributes and un-nonced `` : ""; + const nonceAttr = options.nonce ? ` nonce="${options.nonce}"` : ""; + const scriptTag = options.script ? `${options.script}` : ""; - const bodyContent = options.body ?? `
+ const bodyContent = + options.body ?? + `
Loading preview...
`; diff --git a/packages/blocks/src/sdk/csp.test.ts b/packages/blocks/src/sdk/csp.test.ts new file mode 100644 index 00000000..97a28fb1 --- /dev/null +++ b/packages/blocks/src/sdk/csp.test.ts @@ -0,0 +1,71 @@ +// @vitest-environment node + +import { describe, expect, it } from "vitest"; +import { buildCSPHeaderValue, buildRenderCSP, generateCSPNonce } from "./csp"; + +describe("buildRenderCSP", () => { + const csp = buildRenderCSP({ nonce: "TESTNONCE123" }); + const directives = Object.fromEntries( + csp.split(";").map((d) => { + const [name, ...rest] = d.trim().split(/\s+/); + return [name, rest.join(" ")]; + }), + ); + + it("locks the default source down to nothing", () => { + expect(directives["default-src"]).toBe("'none'"); + }); + + it("authorizes scripts by nonce and NOT by unsafe-inline", () => { + // The whole point: a nonce authorizes only the framework's own + //