import assert from "node:assert"; import { describe, test } from "node:test"; import { HtmlSanitizer } from "../src/index.js"; await describe("HtmlSanitizer", async () => { await describe("Basic Sanitization", async () => { await test("should sanitize simple HTML and preserve safe tags", () => { const html = "

Hello World

"; const sanitized = HtmlSanitizer.sanitize(html); assert.strictEqual(sanitized, "

Hello World

"); }); await test("should preserve nested safe tags", () => { const html = "

Hello World

"; const sanitized = HtmlSanitizer.sanitize(html); assert.strictEqual(sanitized, "

Hello World

"); }); await test("should preserve headings", () => { const html = "

Title

Subtitle

Section

"; const sanitized = HtmlSanitizer.sanitize(html); assert.strictEqual(sanitized, "

Title

Subtitle

Section

"); }); await test("should preserve links with href attribute", () => { const html = "Link"; const sanitized = HtmlSanitizer.sanitize(html); assert.strictEqual(sanitized, "Link"); }); await test("should preserve lists", () => { const html = ""; const sanitized = HtmlSanitizer.sanitize(html); assert.strictEqual(sanitized, ""); }); }); await describe("XSS Attack Prevention", async () => { await test("should remove script tags", () => { const html = "
Hello World
"; const sanitized = HtmlSanitizer.sanitize(html); assert.strictEqual(sanitized, "
Hello World
"); }); await test("should remove inline event handlers", () => { const html = "
Click me
"; const sanitized = HtmlSanitizer.sanitize(html); assert.strictEqual(sanitized, "
Click me
"); }); await test("should remove onerror event from img tags", () => { const html = ""; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(!sanitized.includes("onerror")); assert.ok(!sanitized.includes("alert")); }); await test("should remove javascript: protocol from links", () => { const html = "Click"; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(!sanitized.includes("javascript:")); }); await test("should remove iframe tags", () => { const html = "
Content
"; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(!sanitized.includes("iframe")); assert.strictEqual(sanitized, "
Content
"); }); await test("should remove embed tags", () => { const html = "
Content
"; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(!sanitized.includes("embed")); assert.strictEqual(sanitized, "
Content
"); }); await test("should remove object tags", () => { const html = "
Content
"; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(!sanitized.includes("object")); assert.strictEqual(sanitized, "
Content
"); }); await test("should handle multiple XSS attempts", () => { const html = `
Click Safe content
`; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(!sanitized.includes("onclick")); assert.ok(!sanitized.includes("script")); assert.ok(!sanitized.includes("onerror")); assert.ok(!sanitized.includes("javascript:")); assert.ok(sanitized.includes("Safe content")); }); }); await describe("Image Tag Support", async () => { await test("should preserve img tags with http URLs", () => { const html = "\"Test\""; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(sanitized.includes("img")); assert.ok(sanitized.includes("src=\"http://example.com/image.jpg\"")); }); await test("should preserve img tags with https URLs", () => { const html = "\"Test\""; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(sanitized.includes("img")); assert.ok(sanitized.includes("src=\"https://example.com/image.jpg\"")); }); await test("should preserve img tags with data URLs", () => { const html = "\"Test\""; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(sanitized.includes("img")); assert.ok(sanitized.includes("data:image/png;base64")); }); await test("should preserve img alt attribute", () => { const html = "\"Description\""; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(sanitized.includes("alt=\"Description\"")); }); }); await describe("Style and Class Attributes", async () => { await test("should preserve class attribute on div", () => { const html = "
Content
"; const sanitized = HtmlSanitizer.sanitize(html); assert.strictEqual(sanitized, "
Content
"); }); await test("should preserve style attribute on div", () => { const html = "
Content
"; const sanitized = HtmlSanitizer.sanitize(html); // Note: sanitize-html normalizes CSS formatting assert.ok(sanitized.includes("style=")); assert.ok(sanitized.includes("color")); assert.ok(sanitized.includes("red")); }); await test("should preserve class attribute on paragraph", () => { const html = "

Text

"; const sanitized = HtmlSanitizer.sanitize(html); assert.strictEqual(sanitized, "

Text

"); }); await test("should preserve style attribute on span", () => { const html = "Bold"; const sanitized = HtmlSanitizer.sanitize(html); // Note: sanitize-html normalizes CSS formatting assert.ok(sanitized.includes("style=")); assert.ok(sanitized.includes("font-weight")); assert.ok(sanitized.includes("bold")); }); await test("should preserve both class and style attributes", () => { const html = "
Content
"; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(sanitized.includes("class=\"container\"")); assert.ok(sanitized.includes("style=")); assert.ok(sanitized.includes("margin")); }); await test("should preserve style on img tag", () => { const html = ""; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(sanitized.includes("style=")); assert.ok(sanitized.includes("width")); }); await test("should preserve class on img tag", () => { const html = ""; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(sanitized.includes("class=\"responsive\"")); }); }); await describe("Complex HTML Structures", async () => { await test("should sanitize complex nested HTML", () => { const html = `

Hello World

This is bold text with a link

Example
`; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(sanitized.includes("div")); assert.ok(sanitized.includes("h1")); assert.ok(sanitized.includes("Hello World")); assert.ok(sanitized.includes("bold")); assert.ok(sanitized.includes("href=\"https://example.com\"")); assert.ok(sanitized.includes("img")); assert.ok(sanitized.includes("ul")); assert.ok(sanitized.includes("li")); assert.ok(sanitized.includes("class=\"content\"")); assert.ok(sanitized.includes("style=")); assert.ok(sanitized.includes("padding")); }); await test("should handle mixed safe and unsafe content", () => { const html = `

Title

Paragraph

Safe Link Unsafe Link
`; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(sanitized.includes("

Title

")); assert.ok(sanitized.includes("

Paragraph

")); assert.ok(sanitized.includes("href=\"https://example.com\"")); assert.ok(!sanitized.includes("script")); assert.ok(!sanitized.includes("onclick")); assert.ok(!sanitized.includes("javascript:")); }); }); await describe("Edge Cases", async () => { await test("should throw error for empty string", () => { const html = ""; assert.throws(() => { HtmlSanitizer.sanitize(html); }); }); await test("should handle plain text without HTML tags", () => { const html = "Just plain text"; const sanitized = HtmlSanitizer.sanitize(html); assert.strictEqual(sanitized, "Just plain text"); }); await test("should throw error for HTML with only whitespace", () => { const html = " \n\t "; assert.throws(() => { HtmlSanitizer.sanitize(html); }); }); await test("should handle self-closing tags", () => { const html = "

"; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(sanitized.includes("br")); assert.ok(sanitized.includes("hr")); }); await test("should handle malformed HTML gracefully", () => { const html = "

Unclosed paragraph

Another div
"; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(sanitized.includes("Unclosed paragraph")); assert.ok(sanitized.includes("Another div")); }); await test("should preserve HTML entities", () => { const html = "

<script>alert('test')</script>

"; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(sanitized.includes("<") || sanitized.includes("<")); assert.ok(sanitized.includes(">") || sanitized.includes(">")); }); await test("should handle special characters", () => { const html = "

Price: $100 & €50

"; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(sanitized.includes("$100")); assert.ok(sanitized.includes("€50")); }); }); await describe("Error Handling", async () => { await test("should throw error for null input", () => { assert.throws(() => { HtmlSanitizer.sanitize(null as any); }); }); await test("should throw error for undefined input", () => { assert.throws(() => { HtmlSanitizer.sanitize(undefined as any); }); }); await test("should throw error for non-string input", () => { assert.throws(() => { HtmlSanitizer.sanitize(123 as any); }); }); await test("should throw error for object input", () => { assert.throws(() => { HtmlSanitizer.sanitize({} as any); }); }); await test("should throw error for array input", () => { assert.throws(() => { HtmlSanitizer.sanitize([] as any); }); }); }); await describe("Dangerous CSS Prevention", async () => { await test("should strip position property (clickjacking vector)", () => { const html = "
overlay
"; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(!sanitized.includes("position"), `position should be stripped, got: ${sanitized}`); assert.ok(!sanitized.includes("top:") && !sanitized.includes("top ") || !sanitized.match(/\btop\b\s*:/), "top should be stripped"); assert.ok(!sanitized.match(/\bleft\b\s*:/), "left should be stripped"); }); await test("should strip z-index (clickjacking vector)", () => { const html = "
overlay
"; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(!sanitized.includes("z-index"), `z-index should be stripped, got: ${sanitized}`); }); await test("should strip transform (clickjacking vector)", () => { const html = "
moved
"; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(!sanitized.includes("transform"), `transform should be stripped, got: ${sanitized}`); }); await test("should strip opacity (UI-spoofing vector)", () => { const html = "
invisible
"; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(!sanitized.includes("opacity"), `opacity should be stripped, got: ${sanitized}`); }); await test("should strip background-image (url() exfil vector)", () => { const html = "
x
"; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(!sanitized.includes("background-image"), `background-image should be stripped, got: ${sanitized}`); assert.ok(!sanitized.includes("evil.example"), "url target should not leak into output"); }); await test("should strip background shorthand (url() exfil vector)", () => { const html = "
x
"; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(!sanitized.match(/background\s*:/), `background shorthand should be stripped, got: ${sanitized}`); assert.ok(!sanitized.includes("evil.example"), "url target should not leak into output"); }); await test("should strip cursor (url() exfil vector)", () => { const html = "
x
"; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(!sanitized.includes("cursor"), `cursor should be stripped, got: ${sanitized}`); assert.ok(!sanitized.includes("evil.example")); }); await test("should strip content (url() / attr() exfil vector)", () => { const html = "
x
"; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(!sanitized.match(/\bcontent\s*:/), `content should be stripped, got: ${sanitized}`); }); await test("should strip filter (SVG-filter url() exfil vector)", () => { const html = "
x
"; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(!sanitized.includes("filter"), `filter should be stripped, got: ${sanitized}`); }); await test("should reject display: outside tight enum", () => { const html = "
x
"; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(!sanitized.includes("-webkit-box"), `exotic display value should be rejected, got: ${sanitized}`); assert.ok(sanitized.includes("color"), "safe sibling properties should survive"); }); await test("should accept safe typography alongside rejected dangerous props", () => { const html = "
mix
"; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(sanitized.includes("color"), "color should be preserved"); assert.ok(sanitized.includes("red"), "color value should be preserved"); assert.ok(sanitized.includes("padding"), "padding should be preserved"); assert.ok(!sanitized.includes("position"), "position should be stripped"); assert.ok(!sanitized.includes("z-index"), "z-index should be stripped"); }); }); await describe("Image data URI allowlist", async () => { await test("should strip src for SVG data URI (script-bearing payload)", () => { // base64("") const payload = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIG9ubG9hZD0nYWxlcnQoMSknLz4="; const html = `x`; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(!sanitized.includes("data:image/svg"), `SVG data URI should be stripped, got: ${sanitized}`); assert.ok(!sanitized.includes("onload"), "no onload should leak through"); assert.ok(!sanitized.includes("alert"), "no alert should leak through"); }); await test("should strip src for plain (non-base64) SVG data URI", () => { const payload = "data:image/svg+xml,"; const html = ``; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(!sanitized.includes("data:image/svg"), `SVG data URI should be stripped, got: ${sanitized}`); assert.ok(!sanitized.includes("onload")); }); await test("should strip src for text/html data URI", () => { const payload = "data:text/html,"; const html = ``; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(!sanitized.includes("text/html"), `text/html data URI should be stripped, got: ${sanitized}`); }); await test("should strip src for application/xhtml+xml data URI", () => { const payload = "data:application/xhtml+xml;base64,PGh0bWw+PC9odG1sPg=="; const html = ``; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(!sanitized.includes("application/xhtml"), `xhtml data URI should be stripped, got: ${sanitized}`); }); await test("should preserve PNG data URI", () => { const payload = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="; const html = `px`; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(sanitized.includes("data:image/png"), `PNG data URI should survive, got: ${sanitized}`); }); await test("should preserve JPEG data URI (both jpeg and jpg spellings)", () => { const jpeg = ""; const jpg = ""; assert.ok(HtmlSanitizer.sanitize(jpeg).includes("data:image/jpeg"), "jpeg should survive"); assert.ok(HtmlSanitizer.sanitize(jpg).includes("data:image/jpg"), "jpg should survive"); }); await test("should preserve GIF and WebP data URIs", () => { const gif = ""; const webp = ""; assert.ok(HtmlSanitizer.sanitize(gif).includes("data:image/gif")); assert.ok(HtmlSanitizer.sanitize(webp).includes("data:image/webp")); }); await test("should not affect http/https image URLs", () => { const html = ""; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(sanitized.includes("https://example.com/photo.jpg")); }); await test("should not be fooled by case-varied SVG MIME type", () => { const html = "\" />"; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(!sanitized.toLowerCase().includes("svg"), `case-varied SVG should still be stripped, got: ${sanitized}`); }); await test("should not be fooled by leading whitespace in data URI", () => { const html = "\" />"; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(!sanitized.includes("svg"), `leading-whitespace SVG should be stripped, got: ${sanitized}`); }); await test("should preserve non-src attributes on stripped-src img", () => { const payload = "data:image/svg+xml,"; const html = `keep me`; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(sanitized.includes("alt=\"keep me\""), "alt should survive even when src is stripped"); assert.ok(sanitized.includes("class=\"keep-me\""), "class should survive even when src is stripped"); assert.ok(!sanitized.includes("data:image/svg")); }); }); await describe("Shared-state isolation", async () => { await test("should not mutate sanitize-html library defaults", async () => { const sanitizeHtml = (await import("sanitize-html")).default; const before = JSON.parse(JSON.stringify(sanitizeHtml.defaults.allowedAttributes)); HtmlSanitizer.sanitize("
hi
"); const after = JSON.parse(JSON.stringify(sanitizeHtml.defaults.allowedAttributes)); assert.deepStrictEqual(after, before, "HtmlSanitizer must not mutate sanitize-html shared defaults"); assert.ok(!("*" in sanitizeHtml.defaults.allowedAttributes), "wildcard '*' key should not leak into shared defaults"); }); }); await describe("Real World Examples", async () => { await test("should sanitize user comment with XSS attempt", () => { const html = "Great article! Thanks for sharing."; const sanitized = HtmlSanitizer.sanitize(html); assert.strictEqual(sanitized, "Great article! Thanks for sharing."); }); await test("should sanitize blog post with images", () => { const html = `

My Blog Post

Check out this image:

Photo

Hope you enjoyed it!

`; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(sanitized.includes("class=\"post\"")); assert.ok(sanitized.includes("class=\"title\"")); assert.ok(sanitized.includes("My Blog Post")); assert.ok(sanitized.includes("img")); assert.ok(sanitized.includes("src=\"https://example.com/photo.jpg\"")); assert.ok(sanitized.includes("alt=\"Photo\"")); assert.ok(sanitized.includes("class=\"featured\"")); }); await test("should sanitize email signature", () => { const html = `

John Doe

Software Engineer

john@example.com

`; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(sanitized.includes("John Doe")); assert.ok(sanitized.includes("Software Engineer")); assert.ok(sanitized.includes("href=\"mailto:john@example.com\"")); }); await test("should sanitize rich text editor output", () => { const html = `

Document Title

This is the introduction paragraph.

This is a quote

Visit our website for more.

`; const sanitized = HtmlSanitizer.sanitize(html); assert.ok(sanitized.includes("h1")); assert.ok(sanitized.includes("style=")); assert.ok(sanitized.includes("text-align")); assert.ok(sanitized.includes("class=\"intro\"")); assert.ok(sanitized.includes("ul")); assert.ok(sanitized.includes("li")); assert.ok(sanitized.includes("strong")); assert.ok(sanitized.includes("blockquote")); assert.ok(sanitized.includes("href=\"https://example.com\"")); }); }); });