From b2ef6192ed0caa2665fe1c465579ecc9e3fa4c94 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Tue, 22 Sep 2026 21:41:39 -0400 Subject: [PATCH] fix(media-use): replace regex SVG sanitizer with a DOM allowlist Two rounds of regex/tokenizer patches to sanitizeSvg found new bypasses in the same shape each time (unquoted href, style= url(), aliased xlink prefix; then presentation-attribute url(), unquoted '=' handling, SMIL animate/set href rewrite, self-closing-slash tag corruption, data:image/ navigation via ). Denylisting attribute names against a hand-rolled tokenizer was the wrong approach regardless of how careful the regex. Rewrote sanitizeSvg (packages/core/src/figma/sanitizeSvg.ts) as a real DOM walk (linkedom, already a dependency of core and cli) with an allowlist: known-safe elements and attributes only, everything else dropped by construction rather than pattern-matched as dangerous. href/xlink:href (any namespace prefix) kept only for a same-document fragment, or data:image/ off non- elements. Any attribute value or `, ["evil"]], + [ + "style element with external url()", + ``, + ["evil.example"], + ], + [ + "style element with javascript:", + ``, + ["javascript:"], + ], + [ + "style element with expression()", + ``, + ["expression("], + ], + [ + "foreignObject subtree", + ``, + ["foreignObject", "onload"], + ], + ["self-closing foreignObject", ``, ["foreignObject"]], + ["quoted on* handler", ``, ["onload"]], + ["single-quoted on* handler", ``, ["onload"]], + ["unquoted on* handler", ``, ["onload", "alert"]], + ["mixed-case on* handler", ``, ["ONCLICK", "onclick"]], + [ + "quoted javascript: href", + ``, + ["javascript:"], + ], + [ + "quoted javascript: xlink:href", + ``, + ["javascript:"], + ], + [ + "data:text/html href (not data:image/)", + `x`, + ["data:text"], + ], + ["blob: href", `a`, ["blob:"]], + ["protocol-relative href", `b`, ["evil"]], + [ + "unquoted javascript: href", + ``, + ["javascript:"], + ], + [ + "unquoted external use href", + ``, + ["evil.example"], + ], + [ + "style url() exfil", + ``, + ["evil.example"], + ], + [ + "style @import exfil", + ``, + ["@import", "evil"], + ], + [ + "style expression() exfil", + ``, + ["expression("], + ], + [ + "style javascript: exfil", + ``, + ["javascript:"], + ], + [ + "href via a renamed xlink namespace prefix", + ``, + ["evil.example"], + ], + [ + "fill url() exfil", + ``, + ["evil.example"], + ], + [ + "filter url() exfil", + ``, + ["evil.example"], + ], + [ + "mask url() exfil", + ``, + ["evil.example"], + ], + [ + "clip-path url() exfil", + ``, + ["evil.example"], + ], + [ + "marker-start url() exfil", + ``, + ["evil.example"], + ], + [ + "unquoted attribute value containing '='", + `r`, + ["evil.example"], + ], + [ + "SMIL rewrites href to javascript:", + ``, + ["javascript:", " rewrites href to javascript:", + ``, + ["javascript:", "", + `click`, + ["data:image"], + ], + [ + "unquoted self-closing slash swallow stays contained (no evil URL survives)", + ``, + ["evil.example"], + ], +]; - it("strips foreignObject subtrees", () => { - const dirty = `