feat: introduce local-first SVG workbench

This commit is contained in:
2026-08-02 16:31:49 +02:00
commit 39d9802daa
97 changed files with 20702 additions and 0 deletions
+148
View File
@@ -0,0 +1,148 @@
import { describe, expect, it } from "vitest";
import { parseSvgSource } from "../../src/document/source-parser";
import {
createEditingProjection,
createSanitizedCandidate,
inspectSvgSecurity,
} from "../../src/security/sanitize-svg";
function parse(source: string) {
const result = parseSvgSource(source, 1);
expect(result.semantic).not.toBeNull();
return result.semantic!;
}
describe("hostile SVG editing projection", () => {
it("preserves active content in canonical source but removes it from projection", () => {
const semantic =
parse(`<svg xmlns="http://www.w3.org/2000/svg" onload="steal()">
<script>alert(1)</script>
<foreignObject><div xmlns="http://www.w3.org/1999/xhtml">HTML</div></foreignObject>
<a href="https://evil.example/"><rect width="10" height="10"/></a>
<image href="https://evil.example/tracker.png"/>
<path style="fill:url(https://evil.example/paint)" d="M0 0L1 1"/>
</svg>`);
const findings = inspectSvgSecurity(semantic);
const codes = findings.map((item) => item.code);
expect(codes).toEqual(
expect.arrayContaining([
"event-handler",
"blocked-script",
"blocked-foreignobject",
"unsafe-url",
]),
);
expect(codes.some((code) => code.startsWith("unsafe-inline-css"))).toBe(
true,
);
expect(semantic.source).toContain("steal()");
const projection = createEditingProjection(semantic);
expect(projection.source).not.toMatch(
/<script|foreignObject|steal\(\)|evil\.example/iu,
);
expect(projection.source).toContain("data-svg-tools-node");
});
it("allows local fragment paint and bounded embedded raster data", () => {
const semantic = parse(
`<svg xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="p"/></defs><rect fill="url(#p)"/><image href="data:image/png;base64,iVBORw0KGgo="/></svg>`,
);
const projection = createEditingProjection(semantic);
expect(projection.source).toContain("url(#p)");
expect(projection.source).toContain("data:image/png;base64");
});
it("removes internal mapping metadata from explicit sanitized source", () => {
const semantic = parse(
'<svg xmlns="http://www.w3.org/2000/svg"><circle id="c" r="2"/></svg>',
);
const candidate = createSanitizedCandidate(semantic);
expect(candidate.source).not.toContain("data-svg-tools-node");
expect(candidate.source).toContain('id="c"');
});
it("parses CSS and removes imports, external URLs and behavior-like properties", () => {
const semantic = parse(`<svg xmlns="http://www.w3.org/2000/svg">
<style>@import/**/url(https://evil.example/a.css); .x { fill: red }</style>
<path class="x" style="-moz-binding:url(https://evil.example/x);fill:red" d="M0 0L1 1"/>
</svg>`);
const findings = inspectSvgSecurity(semantic);
expect(
findings.some((item) => item.code.startsWith("unsafe-stylesheet")),
).toBe(true);
expect(
findings.some((item) => item.code.startsWith("unsafe-inline-css")),
).toBe(true);
const projection = createEditingProjection(semantic);
expect(projection.source).not.toContain("evil.example");
expect(projection.source).not.toContain("-moz-binding");
});
it("blocks CSS resource functions even when a URL is represented as a string", () => {
const semantic = parse(
'<svg xmlns="http://www.w3.org/2000/svg"><style>.x { background-image: image-set("tracker.png" 1x) }</style><rect class="x" width="2" height="2"/></svg>',
);
const findings = inspectSvgSecurity(semantic);
expect(findings).toContainEqual(
expect.objectContaining({
code: "unsafe-stylesheet",
description: expect.stringContaining("image-set()"),
}),
);
expect(createEditingProjection(semantic).source).not.toContain(
"tracker.png",
);
});
it("allows only references that resolve to one unique local ID", () => {
const semantic = parse(`<svg xmlns="http://www.w3.org/2000/svg">
<defs><linearGradient id="unique"/><linearGradient id="duplicate"/><linearGradient id="duplicate"/></defs>
<rect id="ok" fill="url(#unique)"/>
<rect id="missing" fill="url(#absent)"/>
<rect id="ambiguous" fill="url(#duplicate)"/>
</svg>`);
const projection = createEditingProjection(semantic).source;
const document = new DOMParser().parseFromString(
projection,
"image/svg+xml",
);
expect(document.getElementById("ok")?.getAttribute("fill")).toBe(
"url(#unique)",
);
expect(document.getElementById("missing")?.hasAttribute("fill")).toBe(
false,
);
expect(document.getElementById("ambiguous")?.hasAttribute("fill")).toBe(
false,
);
});
it("keeps the normal canvas static by removing declarative animation elements", () => {
const semantic = parse(`<svg xmlns="http://www.w3.org/2000/svg">
<circle id="dot" r="4"><animate attributeName="opacity" values="0;1" begin="click"/></circle>
<set attributeName="fill" to="red"/>
</svg>`);
const findings = inspectSvgSecurity(semantic);
expect(findings.map((item) => item.code)).toEqual(
expect.arrayContaining(["blocked-animate", "blocked-set"]),
);
const projection = createEditingProjection(semantic).source;
expect(projection).not.toMatch(/<animate|<set/iu);
expect(projection).toContain('id="dot"');
});
it("reports and removes elements in unknown namespaces", () => {
const semantic =
parse(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:evil="urn:evil">
<evil:widget id="active"><circle r="2"/></evil:widget>
</svg>`);
expect(inspectSvgSecurity(semantic)).toContainEqual(
expect.objectContaining({
code: "unknown-namespace",
editingProjectionAction: "removed",
}),
);
expect(createEditingProjection(semantic).source).not.toContain("widget");
});
});