diff --git a/package.json b/package.json
index 74921c60f5..4d977649ec 100644
--- a/package.json
+++ b/package.json
@@ -51,7 +51,7 @@
"player:perf": "bun run --filter @hyperframes/player perf",
"format:check": "oxfmt --check .",
"knip": "knip",
- "test:scripts": "node --import tsx --test scripts/animejs-v4-guidance.test.mjs scripts/check-tracked-artifacts.test.mjs scripts/check-registry-set-delta.test.mjs scripts/check-no-main-deletions.test.mjs scripts/check-pr-captures.test.mjs scripts/check-docs-snippet-motion.test.mjs scripts/registry-target-paths.test.mjs scripts/check-workspace-contracts.test.mjs scripts/check-media-use-copy-parity.test.mjs scripts/check-svg-sanitize-parity.test.mjs scripts/check-package-cycles.test.mjs scripts/check-cli-process-ownership.test.mjs scripts/check-large-files.test.mjs scripts/package-subpaths.test.mjs scripts/validate-release-channel.test.mjs scripts/publish-workflow.test.mjs scripts/install-workspace-dependencies.test.mjs scripts/draft-changelog.test.ts scripts/set-version.test.ts scripts/release-prepare.test.ts scripts/cli-options.test.ts scripts/changelog-weekly.test.ts scripts/claude-plugin-compression.test.ts scripts/catalog-payload-assets.test.ts scripts/host-registry-assets.test.ts scripts/catalog-preview-temp.test.ts scripts/catalog-hosted-files.test.ts scripts/player-cdn-pin.test.ts scripts/studio-runtime-smoke.test.mjs scripts/verify-packed-manifests.test.mjs scripts/lint-skills.test.mjs scripts/creator-editing-recipes.test.mjs packages/gcp-cloud-run/check-dockerfile-workspaces.test.mjs packages/core/scripts/writeGeneratedFile.test.ts scripts/catalog-drift.test.ts scripts/catalog-fetch-mirror.test.ts scripts/catalog-script-inlining.test.ts scripts/catalog-detail.test.ts scripts/generate-catalog-pages.test.ts scripts/verify-catalog-payloads.test.ts scripts/registry-skill-files.test.ts scripts/creator-editing-capabilities.test.mjs scripts/generate-catalog-previews.test.ts scripts/registry-primitive-payloads.test.ts registry/components/pan-stations/pan-stations.test.mjs && vitest run scripts/catalog/ scripts/contrastRatchet.test.ts scripts/generate-catalog-payloads.test.ts",
+ "test:scripts": "node --import tsx --test scripts/animejs-v4-guidance.test.mjs scripts/check-tracked-artifacts.test.mjs scripts/check-registry-set-delta.test.mjs scripts/check-no-main-deletions.test.mjs scripts/check-pr-captures.test.mjs scripts/check-docs-snippet-motion.test.mjs scripts/registry-target-paths.test.mjs scripts/check-workspace-contracts.test.mjs scripts/check-media-use-copy-parity.test.mjs scripts/check-svg-sanitize-parity.test.mjs scripts/check-media-use-svg-sanitize-generated.test.mjs scripts/check-package-cycles.test.mjs scripts/check-cli-process-ownership.test.mjs scripts/check-large-files.test.mjs scripts/package-subpaths.test.mjs scripts/validate-release-channel.test.mjs scripts/publish-workflow.test.mjs scripts/install-workspace-dependencies.test.mjs scripts/draft-changelog.test.ts scripts/set-version.test.ts scripts/release-prepare.test.ts scripts/cli-options.test.ts scripts/changelog-weekly.test.ts scripts/claude-plugin-compression.test.ts scripts/catalog-payload-assets.test.ts scripts/host-registry-assets.test.ts scripts/catalog-preview-temp.test.ts scripts/catalog-hosted-files.test.ts scripts/player-cdn-pin.test.ts scripts/studio-runtime-smoke.test.mjs scripts/verify-packed-manifests.test.mjs scripts/lint-skills.test.mjs scripts/creator-editing-recipes.test.mjs packages/gcp-cloud-run/check-dockerfile-workspaces.test.mjs packages/core/scripts/writeGeneratedFile.test.ts scripts/catalog-drift.test.ts scripts/catalog-fetch-mirror.test.ts scripts/catalog-script-inlining.test.ts scripts/catalog-detail.test.ts scripts/generate-catalog-pages.test.ts scripts/verify-catalog-payloads.test.ts scripts/registry-skill-files.test.ts scripts/creator-editing-capabilities.test.mjs scripts/generate-catalog-previews.test.ts scripts/registry-primitive-payloads.test.ts registry/components/pan-stations/pan-stations.test.mjs && vitest run scripts/catalog/ scripts/contrastRatchet.test.ts scripts/generate-catalog-payloads.test.ts",
"typecheck:scripts": "tsc --noEmit -p scripts/tsconfig.json",
"test:skills": "node --test 'skills/**/*.test.mjs' 'packages/cli/src/media-use/**/*.test.mjs'",
"generate:previews": "tsx scripts/generate-template-previews.ts",
diff --git a/packages/cli/src/media-use/lib/svg-sanitize.mjs b/packages/cli/src/media-use/lib/svg-sanitize.mjs
index 6fae3a7daf..a1ca1e16ae 100644
--- a/packages/cli/src/media-use/lib/svg-sanitize.mjs
+++ b/packages/cli/src/media-use/lib/svg-sanitize.mjs
@@ -1,64 +1,21 @@
-// Port of packages/core/src/figma/sanitizeSvg.ts; parity enforced by scripts/check-svg-sanitize-parity.test.mjs.
-
-function replaceStable(input, pattern, replacement) {
- let out = input;
- let prev;
- do {
- prev = out;
- out = out.replace(pattern, replacement);
- } while (out !== prev);
- return out;
-}
-
-const TAG_RE = /<([a-zA-Z][\w:-]*)((?:[^"'>]|"[^"]*"|'[^']*')*)>/g;
-const ATTR_RE = /([a-zA-Z_][\w:.-]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]*))/g;
-
-function localName(name) {
- const i = name.lastIndexOf(":");
- return (i === -1 ? name : name.slice(i + 1)).toLowerCase();
-}
-
-function isAllowedHref(value) {
- const v = value.trim().toLowerCase();
- return v.startsWith("#") || v.startsWith("data:image/");
-}
-
-function isDangerousStyle(value) {
- const v = value.toLowerCase();
- return (
- v.includes("url(") ||
- v.includes("@import") ||
- v.includes("expression(") ||
- v.includes("javascript:")
- );
-}
-
-function isDangerousAttr(name, value) {
- const local = localName(name);
- if (local.startsWith("on")) return true;
- if (local === "href") return !isAllowedHref(value);
- if (local === "style") return isDangerousStyle(value);
- return false;
-}
-
-function sanitizeAttributes(svg) {
- return svg.replace(TAG_RE, (tag, tagName, attrs) => {
- const kept = attrs.replace(ATTR_RE, (match, name, dq, sq, uq) => {
- const value = dq ?? sq ?? uq ?? "";
- return isDangerousAttr(name, value) ? "" : match;
- });
- return `<${tagName}${kept}>`;
- });
-}
-
-export function sanitizeSvg(svg) {
- let out = svg;
- out = replaceStable(out, /`;
- const clean = sanitizeSvg(dirty);
- expect(clean).not.toContain("script");
- expect(clean).toContain(" = [
+ ["script element", ``, ["">x`,
+ ["data:text"],
+ ],
+ ["blob: href", ``, ["blob:"]],
+ ["protocol-relative href", ``, ["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 '='",
+ ``,
+ ["evil.example"],
+ ],
+ [
+ "SMIL rewrites href to javascript:",
+ ``,
+ ["javascript:", " rewrites href to javascript:",
+ ``,
+ ["javascript:", "",
+ ``,
+ ["data:image"],
+ ],
+ [
+ "unquoted self-closing slash swallow stays contained (no evil URL survives)",
+ ``,
+ ["evil.example"],
+ ],
+];
- it("strips foreignObject subtrees", () => {
- const dirty = ``;
- const clean = sanitizeSvg(dirty);
- expect(clean).not.toContain("foreignObject");
- expect(clean).not.toContain("iframe");
- expect(clean).toContain(" = [
+ ["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"'],
+ ],
+ [
+ "case-sensitive SVG attributes/elements survive (viewBox, linearGradient)",
+ ``,
+ ['viewBox="0 0 10 10"', 'preserveAspectRatio="xMidYMid"', "