Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/composition/src/visual.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <style> element these declarations are serialized into, and
// a backslash opens a CSS escape that the browser resolves before recognizing functions:
// `u\72 l(...)` reaches the renderer as `url(...)` while evading the checks above.
if (/[<\\]/u.test(value)) {
throw new Error(`${label} contains an unsafe style value.`);
}
if (/(?:var|env|attr)\s*\(/iu.test(value)) {
throw new Error(`${label} contains an environment-dependent style value.`);
}
Expand Down
48 changes: 48 additions & 0 deletions packages/composition/test/track.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,54 @@ test("VisualTrack style values cannot smuggle a second declaration", () => {
);
});

test("VisualTrack style values cannot terminate the compiled style element", () => {
const { timeline, visual } = fixture();
const first = visual.presents[0]!.elements[0]!;
const hostile = sealVisualTrack({...visual,
presents: [{
...visual.presents[0]!,
elements: [{ ...first, style: [{ name: "position", value: "absolute" }, { name: "color", value: "red</style><script>alert(1)</script>" }] }],
}],
});
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</style><script>alert(1)</script>" }] },
] } }],
}],
});
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]!;
Expand Down