diff --git a/packages/core/src/compiler/compositionScoping.test.ts b/packages/core/src/compiler/compositionScoping.test.ts index 7985c9f809..b9d7e72d79 100644 --- a/packages/core/src/compiler/compositionScoping.test.ts +++ b/packages/core/src/compiler/compositionScoping.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import { parseHTML } from "linkedom"; import { + buildVariablesByCompScript, scopeCssToComposition, wrapInlineScriptWithErrorBoundary, wrapScopedCompositionScript, @@ -699,13 +700,18 @@ window.__afterTimeline = window.__timelines.scene; }); it("wraps unscoped composition script source as a string literal", () => { + const source = 'window.payload = "";'; const wrapped = wrapInlineScriptWithErrorBoundary( - 'window.payload = "";', + source, "[HyperFrames] composition script error:", ); expect(wrapped).toContain("Function("); - expect(wrapped).toContain('\\"\\"'); + // The literal carries the source verbatim, with `<` escaped so it cannot end the + // raw-text ` b c" } }; + const body = buildVariablesByCompScript(variables) ?? ""; + const fakeWindow: Record = {}; + new Function("window", body)(fakeWindow); + expect(fakeWindow.__hfVariablesByComp).toEqual(variables); + }); + + it("returns null when there are no per-instance values", () => { + expect(buildVariablesByCompScript({})).toBeNull(); + }); +}); + +/** + * The variables table is not the only attacker-reachable literal emitted into a + * `` would close + * the element early and have the remainder parsed as markup. Rewriting every + * `<` to `<` removes the only byte that can start a closing tag, and is + * transparent to both `JSON.parse` and the JS string grammar, so the value the + * runtime reads is unchanged. + * + * Every dynamic literal in an emitted script body must go through here: a + * per-value guard on this surface has already been missed once, since the + * composition id reaches the emitted script through four separate literals. + */ +function jsonScriptLiteral(value: unknown): string { + return JSON.stringify(value).replace(/>, ): string | null { if (!variablesByComp || Object.keys(variablesByComp).length === 0) return null; - return `window.__hfVariablesByComp = Object.assign({}, window.__hfVariablesByComp || {}, ${JSON.stringify(variablesByComp)});`; + const json = jsonScriptLiteral(variablesByComp); + return `window.__hfVariablesByComp = Object.assign({}, window.__hfVariablesByComp || {}, ${json});`; }