fix(media-use): close three sanitizer bypasses, add fetch timeout

Independent review of the maintainer pass found the SVG sanitizer
(packages/core/src/figma/sanitizeSvg.ts, reused by media-use's own
port) was itself bypassable: an unquoted href/xlink:href survived,
inline style="..." attributes were never checked, and a href bound
through a renamed XML namespace prefix (xmlns:x=".../1999/xlink",
x:href=...) slipped past the literal "xlink:href" match.

Rewrote sanitizeSvg as a tag/attribute tokenizer instead of a set of
literal-spelling regexes: parse each tag's attributes in any quoting
form, resolve the local name after stripping any namespace prefix,
then drop by local name (any on* handler, any *href whose value is
not a #fragment or data:image/ embed, any style attribute containing
url(, @import, expression( or javascript:). Ported the fix into
media-use's svg-sanitize.mjs identically. Legitimate url(#id)
references on non-style attributes (clip-path, fill) are untouched.

Also adds a 10s AbortSignal.timeout to freezeUrl's fetch, matching
every other fetchMedia call in this package - it had a size cap but
no deadline.

New cases for all three bypasses in both sanitizeSvg.test.ts and the
parity corpus, each reproducibly failing against the prior head.
This commit is contained in:
Miguel Angel Simon Sierra
2026-09-23 02:09:48 -04:00
parent d570e96f5d
commit a12a50fc45
5 changed files with 147 additions and 44 deletions
@@ -35,6 +35,22 @@ const HOSTILE_CORPUS = [
],
["<svg><rect onMouseOver=\"alert('x')\" ONCLICK='y'/></svg>", "mixed-case on* handler names"],
['<svg><a HREF="javascript:alert(1)"><rect/></a></svg>', "mixed-case href attribute name"],
// Reported against b7ace99b4 — see hyperframes-4291-b7ace99b.md BLOCKER 1-3.
["<svg><a href=javascript:alert(1)><rect/></a></svg>", "unquoted javascript: href"],
["<svg><use href=https://evil.example/x.svg#p/></svg>", "unquoted external use href"],
['<svg><rect style="fill:url(https://evil.example/x)"/></svg>', "style url() exfil"],
['<svg><rect style="background:@import url(evil)"/></svg>', "style @import exfil"],
['<svg><rect style="width:expression(alert(1))"/></svg>', "style expression() exfil"],
[`<svg><rect style="background:url('javascript:alert(1)')"/></svg>`, "style javascript: exfil"],
['<svg><rect style="fill:#123456"/></svg>', "benign style attribute survives"],
[
'<svg><path clip-path="url(#clip0)" fill="url(#grad)"/></svg>',
"url(#id) on non-style attrs survives",
],
[
'<svg xmlns:x="http://www.w3.org/1999/xlink"><use x:href="https://evil.example/x.svg#p"/></svg>',
"href via a renamed xlink namespace prefix",
],
];
describe("svg-sanitize parity: core vs media-use port", () => {