Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/utils/html-sanitizer.test.ts
T

106 lines
4.4 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { renderSanitizedMarkdown, sanitizeIconSvg, sanitizeMarkedHtml } from "./html-sanitizer";
describe("html sanitizer", () => {
it("removes active HTML and unsafe URLs from marked output", () => {
const html = sanitizeMarkedHtml(`
<h1 onclick="alert(1)">Title</h1>
<script>alert(1)</script>
<style>body { color: red; }</style>
<iframe src="https://example.com"></iframe>
<object data="https://example.com"></object>
<embed src="https://example.com" />
<link rel="preload" href="https://example.com" />
<meta http-equiv="refresh" content="0" />
<base href="https://example.com" />
<svg onload="alert(1)"><circle /></svg>
<a href="javascript:alert(1)">bad</a>
<a href="data:text/vbscript,alert(1)">also bad</a>
<a href="https://example.com/page" rel="ugc">external</a>
`);
expect(html).not.toMatch(/<script|<style|<iframe|<object|<embed|<link|<meta|<base|<svg/i);
expect(html).not.toMatch(/onclick|javascript:|data:text\/vbscript/i);
expect(html).toContain('<a href="https://example.com/page" rel="ugc noopener noreferrer">external</a>');
});
it("sanitizes markdown links before rendering previews", () => {
const html = renderSanitizedMarkdown(`[unsafe](javascript:alert(1)) [safe](https://example.com)`);
expect(html).not.toMatch(/javascript:/i);
expect(html).toContain('href="https://example.com" rel="noopener noreferrer"');
});
it("escapes author-typed raw HTML in markdown instead of passing it through", () => {
const html = renderSanitizedMarkdown(`<div><img src="x"/onerror=alert(1)></div>`);
// The dangerous markup must survive only as inert escaped text, never as a live tag.
expect(html).not.toMatch(/<img/i);
expect(html).not.toMatch(/<div/i);
expect(html).toContain("&lt;img");
// legitimate markdown still renders
expect(renderSanitizedMarkdown("**bold**")).toContain("<strong>bold</strong>");
});
describe("server string fallback (no DOMParser)", () => {
const originalDomParser = globalThis.DOMParser;
beforeEach(() => {
// Force the SSR code path that does not have a real HTML parser.
delete (globalThis as { DOMParser?: unknown }).DOMParser;
});
afterEach(() => {
globalThis.DOMParser = originalDomParser;
});
it("strips slash-separated event handlers that the previous regex missed", () => {
const html = sanitizeMarkedHtml(`<img src="x"/onerror=alert(1)>`);
expect(html).not.toMatch(/onerror/i);
});
it("strips whitespace-separated event handlers and unsafe protocols", () => {
const html = sanitizeMarkedHtml(`<img src="x" onerror="alert(1)"><a href="javascript:alert(1)">x</a>`);
expect(html).not.toMatch(/onerror/i);
expect(html).not.toMatch(/javascript:/i);
});
it("keeps the markdown render path safe even without a DOM parser", () => {
const html = renderSanitizedMarkdown(`<img/src=x/onerror=alert(1)>`);
expect(html).not.toMatch(/<img/i);
expect(html).not.toMatch(/onerror=/i);
});
it("sanitizes inline svg icons without a DOM parser while keeping the drawing", () => {
const svg = sanitizeIconSvg(
`<svg viewBox="0 0 10 10" onload="alert(1)"><script>alert(1)</script><a xlink:href="javascript:alert(1)"><rect width="9" height="9"/></a></svg>`,
"h-5 w-5"
);
expect(svg).not.toMatch(/onload/i);
expect(svg).not.toMatch(/<script/i);
expect(svg).not.toMatch(/javascript:/i);
expect(svg).toMatch(/<svg/i);
expect(svg).toContain("rect");
expect(svg).toContain('class="h-5 w-5"');
});
});
describe("sanitizeIconSvg", () => {
it("strips scriptable svg while preserving paths", () => {
const svg = sanitizeIconSvg(
`<svg viewBox="0 0 24 24"><path d="M1 1"/><foreignObject><img src=x onerror=alert(1)></foreignObject></svg>`,
"size-4"
);
expect(svg).not.toMatch(/onerror/i);
expect(svg).not.toMatch(/foreignobject/i);
expect(svg).toContain('d="M1 1"');
expect(svg).toContain('class="size-4"');
});
it("returns empty string for non-svg input", () => {
expect(sanitizeIconSvg("<img src=x onerror=alert(1)>")).toBe("");
expect(sanitizeIconSvg("https://example.com/icon.png")).toBe("");
expect(sanitizeIconSvg(null)).toBe("");
});
});
});