- Detect SVG by content (magic bytes) in addition to MIME type to prevent sanitization bypass via mislabeled uploads - Add guard test verifying DOMPurify actually strips dirty payloads - Add looksLikeSvg detection test - Document REDIS_CACHE_DB in .env.example
96 lines
3.6 KiB
TypeScript
96 lines
3.6 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { looksLikeSvg, purify, sanitizeSvg, svgSanitizeOptions } from "./svg-purify";
|
|
|
|
describe("SVG sanitization", () => {
|
|
it("strips script tags from SVG", () => {
|
|
const dirty = '<svg><script>alert("xss")</script><circle r="10"/></svg>';
|
|
const clean = sanitizeSvg(dirty);
|
|
expect(clean).not.toContain("<script");
|
|
expect(clean).toContain("circle");
|
|
});
|
|
|
|
it("strips onload event handler", () => {
|
|
const dirty = '<svg onload="alert(1)"><rect width="100" height="100"/></svg>';
|
|
const clean = sanitizeSvg(dirty);
|
|
expect(clean).not.toContain("onload");
|
|
});
|
|
|
|
it("strips data URI in use href", () => {
|
|
const dirty = '<svg><use href="data:text/html,<script>alert(1)</script>"/></svg>';
|
|
const clean = sanitizeSvg(dirty);
|
|
expect(clean).not.toContain("data:");
|
|
});
|
|
|
|
it("strips foreignObject", () => {
|
|
const dirty =
|
|
'<svg><foreignObject><body xmlns="http://www.w3.org/1999/xhtml"><script>alert(1)</script></body></foreignObject></svg>';
|
|
const clean = sanitizeSvg(dirty);
|
|
expect(clean).not.toContain("foreignObject");
|
|
});
|
|
|
|
it("preserves safe SVG content", () => {
|
|
const safe = '<svg viewBox="0 0 100 100"><circle cx="50" cy="50" r="40" fill="red"/></svg>';
|
|
const clean = sanitizeSvg(safe);
|
|
expect(clean).toContain("circle");
|
|
expect(clean).toContain('fill="red"');
|
|
});
|
|
|
|
it("uses the same DOMPurify options as production", () => {
|
|
const dirty = '<svg><script>alert("xss")</script></svg>';
|
|
const wrapped = `<html><body>${dirty}</body></html>`;
|
|
expect(sanitizeSvg(dirty)).toBe(purify.sanitize(wrapped, svgSanitizeOptions));
|
|
});
|
|
|
|
it("strips javascript: protocol in href", () => {
|
|
const dirty = '<svg><a href="javascript:alert(1)"><text>click</text></a></svg>';
|
|
const clean = sanitizeSvg(dirty);
|
|
expect(clean).not.toContain("javascript:");
|
|
});
|
|
|
|
it("strips xlink:href with javascript:", () => {
|
|
const dirty = '<svg><a xlink:href="javascript:alert(1)"><text>click</text></a></svg>';
|
|
const clean = sanitizeSvg(dirty);
|
|
expect(clean).not.toContain("javascript:");
|
|
});
|
|
|
|
it("strips style with url() import", () => {
|
|
const dirty =
|
|
'<svg><style>@import url("https://evil.com/steal.css");</style><rect width="100" height="100"/></svg>';
|
|
const clean = sanitizeSvg(dirty);
|
|
expect(clean).not.toContain("@import");
|
|
});
|
|
|
|
it("strips animate with onbegin handler", () => {
|
|
const dirty = '<svg><animate onbegin="alert(1)"/></svg>';
|
|
const clean = sanitizeSvg(dirty);
|
|
expect(clean).not.toContain("onbegin");
|
|
});
|
|
|
|
it("DOMPurify actually strips dirty payload (guard against no-op shim)", () => {
|
|
const dirty = '<svg><script>alert("xss")</script><rect width="1" height="1"/></svg>';
|
|
const clean = sanitizeSvg(dirty);
|
|
expect(clean).not.toBe(dirty);
|
|
expect(clean).not.toContain("<script");
|
|
expect(clean).toContain("<rect");
|
|
});
|
|
|
|
it("looksLikeSvg detects SVG content regardless of MIME", () => {
|
|
const svg = new TextEncoder().encode('<svg viewBox="0 0 24 24"><path d="M0 0"/></svg>');
|
|
const png = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
|
expect(looksLikeSvg(svg)).toBe(true);
|
|
expect(looksLikeSvg(png)).toBe(false);
|
|
});
|
|
|
|
it("preserves well-formed SVG with self-closing children", () => {
|
|
const icon =
|
|
'<svg viewBox="0 0 24 24"><path d="M12 2L2 22h20Z" fill="currentColor"/><circle cx="12" cy="8" r="2"/></svg>';
|
|
const clean = sanitizeSvg(icon);
|
|
expect(clean).toContain("<path");
|
|
expect(clean).toContain("<circle");
|
|
expect(clean).toContain("</svg>");
|
|
expect(clean).not.toContain("<html");
|
|
expect(clean).not.toContain("<body");
|
|
});
|
|
});
|