import { gzipSync } from "node:zlib"; import { describe, expect, it, vi } from "vitest"; vi.mock("../../../apps/api/src/config.js", () => ({ env: { MAX_SVG_SIZE_MB: 10 }, })); import { decompressSvgz, isSvgBuffer, sanitizeSvg, } from "../../../apps/api/src/lib/svg-sanitize.js"; describe("sanitizeSvg", () => { it("removes DOCTYPE declarations", () => { const input = Buffer.from( '', ); const result = sanitizeSvg(input).toString("utf-8"); expect(result).not.toContain("DOCTYPE"); expect(result).toContain(""); }); it("removes DOCTYPE with internal subset (XXE prevention)", () => { const input = Buffer.from( ']>&xxe;', ); const result = sanitizeSvg(input).toString("utf-8"); expect(result).not.toContain("DOCTYPE"); expect(result).not.toContain("ENTITY"); }); it("removes script tags", () => { const input = Buffer.from( '', ); const result = sanitizeSvg(input).toString("utf-8"); expect(result).not.toContain(" { const input = Buffer.from("evil"); const result = sanitizeSvg(input).toString("utf-8"); expect(result).not.toContain("foreignObject"); expect(result).toContain(""); }); it("removes event handlers (onload, onclick, onerror)", () => { const input = Buffer.from( '', ); const result = sanitizeSvg(input).toString("utf-8"); expect(result).not.toContain("onload="); expect(result).not.toContain("onclick="); expect(result).not.toContain("onerror="); }); it("blocks javascript: URIs in href", () => { const input = Buffer.from('click'); const result = sanitizeSvg(input).toString("utf-8"); expect(result).not.toContain("javascript:"); }); it("blocks http/https URLs in href", () => { const input = Buffer.from(''); const result = sanitizeSvg(input).toString("utf-8"); expect(result).not.toContain("https://evil.com"); }); it("blocks http/https URLs in xlink:href", () => { const input = Buffer.from(''); const result = sanitizeSvg(input).toString("utf-8"); expect(result).not.toContain("http://evil.com"); }); it("blocks data:text/html URIs", () => { const input = Buffer.from( 'x', ); const result = sanitizeSvg(input).toString("utf-8"); expect(result).not.toContain("data:text/html"); }); it("blocks file: URIs", () => { const input = Buffer.from(''); const result = sanitizeSvg(input).toString("utf-8"); expect(result).not.toContain("file:///etc/passwd"); }); it("blocks external url() references", () => { const input = Buffer.from(""); const result = sanitizeSvg(input).toString("utf-8"); expect(result).not.toContain("https://evil.com"); }); it("blocks file: url() references", () => { const input = Buffer.from(''); const result = sanitizeSvg(input).toString("utf-8"); expect(result).not.toContain("file:///etc/passwd"); }); it("removes XInclude elements", () => { const input = Buffer.from( '', ); const result = sanitizeSvg(input).toString("utf-8"); expect(result).not.toContain("xi:include"); }); it("preserves valid SVG content", () => { const input = Buffer.from( '', ); const result = sanitizeSvg(input).toString("utf-8"); expect(result).toContain(" { const configMod = await import("../../../apps/api/src/config.js"); const saved = configMod.env.MAX_SVG_SIZE_MB; configMod.env.MAX_SVG_SIZE_MB = 0.000001; try { const input = Buffer.from(''); expect(() => sanitizeSvg(input)).toThrow("SVG exceeds maximum size"); } finally { configMod.env.MAX_SVG_SIZE_MB = saved; } }); it("does NOT remove xml processing instruction", () => { const input = Buffer.from(''); const result = sanitizeSvg(input).toString("utf-8"); expect(result).toContain(" { it("returns non-gzipped buffer unchanged", () => { const input = Buffer.from(''); const result = decompressSvgz(input); expect(result).toBe(input); }); it("decompresses valid SVGZ content", () => { const svgContent = ''; const compressed = gzipSync(Buffer.from(svgContent)); const result = decompressSvgz(compressed); expect(result.toString("utf-8")).toBe(svgContent); }); it("throws on decompressed content that is not SVG", () => { const notSvg = gzipSync(Buffer.from("this is just plain text, not SVG at all")); expect(() => decompressSvgz(notSvg)).toThrow("does not contain valid SVG"); }); it("returns short buffer unchanged (less than 2 bytes)", () => { const tiny = Buffer.from([0x42]); expect(decompressSvgz(tiny)).toBe(tiny); }); it("returns buffer unchanged when first bytes do not match gzip magic", () => { const notGzip = Buffer.from([0x50, 0x4b, 0x03, 0x04]); expect(decompressSvgz(notGzip)).toBe(notGzip); }); }); describe("isSvgBuffer", () => { it("returns true for buffer starting with { const input = Buffer.from(''); expect(isSvgBuffer(input)).toBe(true); }); it("returns true for buffer starting with { const input = Buffer.from(''); expect(isSvgBuffer(input)).toBe(true); }); it("returns false for PNG buffer", () => { const png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); expect(isSvgBuffer(png)).toBe(false); }); it("returns false for empty buffer", () => { expect(isSvgBuffer(Buffer.alloc(0))).toBe(false); }); it("returns false for plain text buffer", () => { const text = Buffer.from("Hello, this is not an SVG file"); expect(isSvgBuffer(text)).toBe(false); }); it("returns true for SVG with leading whitespace", () => { const input = Buffer.from(' \n '); expect(isSvgBuffer(input)).toBe(true); }); it("returns true for { const input = Buffer.from('\n'); expect(isSvgBuffer(input)).toBe(true); }); });