73 lines
3.2 KiB
TypeScript
73 lines
3.2 KiB
TypeScript
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:");
|
|
});
|
|
});
|