From 9a7ace9e92e5252cabb8afd6e34b94878f9f9ea2 Mon Sep 17 00:00:00 2001 From: Jaynel Patiarba Date: Wed, 26 Aug 2026 21:31:04 +0800 Subject: [PATCH] fix(security): encode values in inline `/`` breaks out of the tag and injects markup. React does not escape dangerouslySetInnerHTML. Reachable both via stored CMS content (live page) and reflected via the unauthenticated /deco/render preview endpoint. Add context-aware encoders in @decocms/blocks/sdk/htmlSafe: - htmlSafeJson — JSON embedded in "; + +describe("Seo — JSON-LD script sink", () => { + it("does not let a jsonLDs value break out of the ld+json script", () => { + const html = renderToStaticMarkup( + ${SCRIPT_INJECT}` }]} />, + ); + expect(html).not.toContain(SCRIPT_INJECT); + expect(html).not.toContain("` must never break out of the +// tag. Asserting on the rendered HTML is the only faithful check — tsc and the +// component's types don't catch it (React does not escape dangerouslySetInnerHTML). + +const BREAKOUT = ""; +const INJECTED = ""; + +describe("ProductJsonLd — JSON-LD script sink", () => { + it("does not let a product name break out of the ld+json script", () => { + const html = renderToStaticMarkup(); + expect(html).not.toContain(INJECTED); + expect(html).not.toContain(""; +const LS = String.fromCharCode(0x2028); // line separator — breaks inline scripts +const PS = String.fromCharCode(0x2029); // paragraph separator + +describe("htmlSafeJson — JSON embedded in breakout inside a string value", () => { + const out = htmlSafeJson({ name: BREAKOUT }); + // The literal tag terminator must never survive into the HTML stream. + expect(out).not.toContain(""); + expect(out).not.toContain("<"); + expect(out).not.toContain(">"); + }); + + it("stays valid JSON that parses back to the original value", () => { + const data = { name: BREAKOUT, n: 1, nested: { u: `a${LS}b` } }; + expect(JSON.parse(htmlSafeJson(data))).toEqual(data); + }); + + it("escapes the line/paragraph separators that break inline scripts", () => { + const out = htmlSafeJson({ s: `a${LS}b${PS}c` }); + expect(out).not.toContain(LS); + expect(out).not.toContain(PS); + }); +}); + +describe("jsString — value interpolated into a single-quoted JS string", () => { + it("neutralizes a breakout", () => { + const emitted = `posthog.init('${jsString(BREAKOUT)}')`; + expect(emitted).not.toContain(""); + expect(emitted).not.toContain("<"); + }); + + it("neutralizes a single-quote string-breakout", () => { + const esc = jsString("');alert(1);('"); + // The security invariant: no single quote may appear UN-escaped, so the + // payload can never close the surrounding '...' literal early. + expect(esc).not.toMatch(/(^|[^\\])'/); + expect(esc).toContain("\\'"); + }); + + it("leaves a benign value readable", () => { + expect(jsString("phc_abc123")).toBe("phc_abc123"); + }); +}); + +describe("cssSafe — value interpolated into a breakout", () => { + const out = cssSafe("red}"); + expect(out).not.toContain(""); + expect(out).not.toContain("<"); + expect(out).not.toContain(">"); + }); + + it("leaves a benign CSS value intact", () => { + expect(cssSafe("#fff")).toBe("#fff"); + }); +}); diff --git a/packages/blocks/src/sdk/htmlSafe.ts b/packages/blocks/src/sdk/htmlSafe.ts new file mode 100644 index 00000000..efafa04d --- /dev/null +++ b/packages/blocks/src/sdk/htmlSafe.ts @@ -0,0 +1,62 @@ +/** + * Context-aware escaping for values interpolated into inline `` / `` regardless of quoting or JSON context. + * `JSON.stringify` escapes JSON metacharacters but NOT `<`, so a string value + * containing `` breaks out of the tag and injects markup. React does + * not escape inside `dangerouslySetInnerHTML`. These helpers close that class: + * always run untrusted (or possibly-untrusted) values through the matching + * helper for the surrounding context, never bare `JSON.stringify`/interpolation. + */ + +// Built via RegExp() so the source file never contains a raw U+2028/U+2029 +// byte — those are JS line terminators and would break the parser here. +const LINE_SEP = new RegExp("\\u2028", "g"); +const PARA_SEP = new RegExp("\\u2029", "g"); + +/** + * Serialize a value to JSON that is safe to embed directly in a `` (or `