From d49e7b95e0b0a6577a42710ad2a7e0ca93280866 Mon Sep 17 00:00:00 2001 From: SauceXStudios Date: Wed, 23 Sep 2026 13:56:16 -0700 Subject: [PATCH] feat(media-use): resolve brand logos from theSVG (#4291) * feat(media-use): resolve brand logos from theSVG Replace the svgl and simple-icons tiers of the `logo` cascade with a single theSVG (thesvg.org) tier. theSVG ships 7,400+ official brand SVGs with color, mono, light/dark and wordmark variants. The tier fetches one manifest pinned to a glincker/thesvg commit on jsDelivr, memoizes it per process, and matches entities locally by normalized slug, title, or alias. That drops the hand-kept svgl alias and simple-icons slug maps. GitHub org avatar and favicon stay as the fallbacks for brands theSVG lacks. Co-Authored-By: Claude Opus 5.5 * fix(media-use): sanitize fetched SVGs and gate theSVG by license freezeUrl/freezeLocalFile wrote every fetched SVG to disk unsanitized; the theSVG tier is now the sole SVG-returning logo provider, so an inline script tag or on-event handler would land verbatim in a user project. Route .svg destinations through the existing Figma sanitizer (new @hyperframes/core/sanitize-svg subpath, synced via package-subpaths). thesvgMatch never checked theSVG's own license field: entries theSVG itself marks unresolved (Proprietary, Unknown, Custom, TODO, or an explicit no-redistribution-license string) could still resolve and freeze into a project. Gate matches to the ten SPDX ids theSVG's submission form accepts; a rejected entry falls through to a lower ranked theSVG match, then to the github/favicon tiers. Also caps the theSVG manifest fetch at 20MB, streamed, matching freeze.mjs's existing cap discipline. It had a timeout but no bound. * chore(skills): regenerate skills-manifest for media-use/motion-graphics The original submission edited skills/media-use and skills/motion-graphics docs without regenerating skills-manifest.json; its own CI never ran (fork approval gate), so the drift went unnoticed until this pass's first CI run caught it. * fix(media-use): drop the sanitizer's workspace import, add parity test freeze.mjs importing @hyperframes/core/sanitize-svg broke the test-skills CI lane: packages/cli/src/media-use tests run with node builtins only, zero install, by design (media-use is vendored standalone into other projects, e.g. packages/desktop). Port the sanitizer into media-use/lib/svg-sanitize.mjs instead, and drop the now-unused core export. Parity between the port and packages/core/src/figma/sanitizeSvg.ts is enforced by a new test (scripts/check-svg-sanitize-parity.test.mjs, run in the test:scripts lane, which does have an install) over a hostile-SVG corpus, so the two cannot drift silently. * 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. * 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("], + ], + [ + "CSS-escaped url() in style=", + String.raw``, + ["evil.example"], + ], + [ + "CSS-escaped url() in a presentation attribute", + String.raw``, + ["evil.example"], + ], + [ + "CSS-escaped parens around a url in style=", + String.raw``, + ["evil.example"], + ], + [ + "CSS-escaped url() in a style element", + String.raw``, + ["evil.example"], + ], + [ + "CSS-escaped @import in a style element", + String.raw``, + ["evil.example"], + ], + [ + "image-set() string URL in a style element (no url() at all)", + ``, + ["evil.example"], + ], + [ + "CSS-escaped image-set() in style=", + String.raw``, + ["evil.example"], + ], + [ + "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"], + ], +]; + +const LEGITIMATE_CASES: Array<[label: string, input: string, mustContain: string[]]> = [ + ["local fragment href", ``, ['href="#localClip"']], + [ + "data:image href kept off (image fetch context, not a navigation)", + ``, + ["data:image/png;base64,AAAA"], + ], + ["xlink:href local fragment", ``, ['href="#p"']], + [ + "url(#id) on presentation attributes (clip-path, fill, marker-start)", + ``, + ['clip-path="url(#clip0)"', 'fill="url(#grad)"', 'marker-start="url(#arrow)"'], + ], + ["benign style attribute", ``, ['style="fill:#123456"']], + [ + "benign `, + [".st0{fill:#5757F5;}", 'class="st0"'], + ], + [ + "compute-only CSS functions survive (real theSVG shapes: display-p3 color, transforms, @media)", + ``, + ["@media (prefers-color-scheme:dark)", "color(display-p3 0.92 0.94 0.94)", "skewX(4)"], + ], + [ + "case-sensitive SVG attributes/elements survive (viewBox, linearGradient)", + ``, + ['viewBox="0 0 10 10"', 'preserveAspectRatio="xMidYMid"', "`, + ['fill="red"'], + ], +]; + +describe("sanitizeSvg: hostile input", () => { + for (const [label, input, mustNotContain] of HOSTILE_CASES) { + it(`drops: ${label}`, () => { + const clean = sanitizeSvg(input); + for (const needle of mustNotContain) + expect(clean, `${label}: ${clean}`).not.toContain(needle); + }); + } +}); + +describe("sanitizeSvg: legitimate content survives", () => { + for (const [label, input, mustContain] of LEGITIMATE_CASES) { + it(`keeps: ${label}`, () => { + const clean = sanitizeSvg(input); + for (const needle of mustContain) expect(clean, `${label}: ${clean}`).toContain(needle); + }); + } + + it("a typical clean figma export keeps its structure and attributes", () => { + const dirty = ``; const clean = sanitizeSvg(dirty); - expect(clean).not.toContain("script"); - expect(clean).toContain(" { - const dirty = `