diff --git a/packages/composition/src/visual.ts b/packages/composition/src/visual.ts index e4308dbb5..6a0f9468f 100644 --- a/packages/composition/src/visual.ts +++ b/packages/composition/src/visual.ts @@ -216,6 +216,12 @@ export function assertVisualStyleV1( if (/!important|[\u0000-\u001f\u007f]/iu.test(value)) { throw new Error(`${label} contains an unsafe style value.`); } + // `<` terminates the raw-text " }] }], + }], + }); + assert.throws( + () => assertVisualTrackIdentity(hostile, timeline), + /unsafe style value/, + ); +}); + +test("VisualTrack style values cannot hide resource functions behind CSS escapes", () => { + const { timeline, visual } = fixture(); + const first = visual.presents[0]!.elements[0]!; + const escaped = sealVisualTrack({...visual, + presents: [{ + ...visual.presents[0]!, + elements: [{ ...first, style: [{ name: "background", value: "u\\72 l(https://invalid.example/)" }] }], + }], + }); + assert.throws( + () => assertVisualTrackIdentity(escaped, timeline), + /unsafe style value/, + ); +}); + +test("VisualTrack animation keyframes share the same unsafe-value checks", () => { + const { timeline, visual } = fixture(); + const first = visual.presents[0]!.elements[0]!; + const hostile = sealVisualTrack({...visual, + presents: [{ + ...visual.presents[0]!, + elements: [{ ...first, animation: { keyframes: [ + { atFrame: 0, style: [{ name: "opacity", value: 1 }] }, + { atFrame: 30, style: [{ name: "opacity", value: "0" }] }, + ] } }], + }], + }); + assert.throws( + () => assertVisualTrackIdentity(hostile, timeline), + /unsafe style value/, + ); +}); + test("VisualTrack cannot silently extend the versioned public Visual IR", () => { const { timeline, visual } = fixture(); const first = visual.presents[0]!.elements[0]!;