feat: introduce local-first SVG workbench
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseSvgSource } from "../../src/document/source-parser";
|
||||
import {
|
||||
createEditingProjection,
|
||||
inspectSvgSecurity,
|
||||
} from "../../src/security/sanitize-svg";
|
||||
|
||||
function parse(source: string) {
|
||||
const result = parseSvgSource(source, 1);
|
||||
expect(result.semantic).not.toBeNull();
|
||||
return result.semantic!;
|
||||
}
|
||||
|
||||
describe("application-owned adversarial SVG policy", () => {
|
||||
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;
|
||||
expect(projection).toContain('id="ok" fill="url(#unique)"');
|
||||
expect(projection).not.toContain("url(#absent)");
|
||||
expect(projection).not.toContain('id="ambiguous" fill=');
|
||||
});
|
||||
|
||||
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 "https://evil.example/theme.css"; rect { fill: red }</style>
|
||||
<rect id="external" style="fill: url(https://evil.example/a); stroke: red"/>
|
||||
<circle id="behavior" style="behavior: url(#x)"/>
|
||||
</svg>`);
|
||||
const findings = inspectSvgSecurity(semantic);
|
||||
expect(findings.map((item) => item.code)).toEqual(
|
||||
expect.arrayContaining(["unsafe-stylesheet", "unsafe-inline-css"]),
|
||||
);
|
||||
const projection = createEditingProjection(semantic).source;
|
||||
expect(projection).not.toContain("evil.example");
|
||||
expect(projection).not.toContain("behavior:");
|
||||
expect(projection).not.toContain("<style");
|
||||
});
|
||||
|
||||
it("keeps the normal editing canvas static by stripping source animations", () => {
|
||||
const semantic = parse(`<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<circle id="dot" r="2"><animate attributeName="opacity" values="0;1" dur="1s"/></circle>
|
||||
<set attributeName="fill" to="red" begin="click"/>
|
||||
</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\b/iu);
|
||||
expect(semantic.source).toContain("<animate");
|
||||
});
|
||||
|
||||
it("neutralizes executable schemes, external use targets and non-SVG element namespaces", () => {
|
||||
const semantic =
|
||||
parse(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:x="urn:active">
|
||||
<a href="javascript:alert(1)"><rect width="1" height="1"/></a>
|
||||
<use href="https://evil.example/icons.svg#mark"/>
|
||||
<x:widget x:run="yes"/>
|
||||
<image href="data:text/html;base64,PHNjcmlwdD4="/>
|
||||
</svg>`);
|
||||
const projection = createEditingProjection(semantic).source;
|
||||
expect(projection).not.toMatch(
|
||||
/javascript:|evil\.example|data:text\/html|x:widget/iu,
|
||||
);
|
||||
expect(semantic.source).toContain("javascript:");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user