#21: add explicit DOMPurify HTML sanitizer test suite and refine remote image blocker
This commit is contained in:
1 parent
1707ade6da
commit
86aad5087b
2 files changed
+65
-21
No files matched your search
@@ -5,28 +5,36 @@ import DOMPurify from "dompurify";
|
||||
* Strips scripts, dangerous attributes, and executable elements while keeping layout.
|
||||
*/
|
||||
export function sanitizeEmailHtml(rawHtml: string): string {
|
||||
if (typeof window === "undefined") {
|
||||
// SSR fallback: basic strip of script tags
|
||||
return rawHtml.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "");
|
||||
if (!rawHtml) return "";
|
||||
|
||||
// Base sanitization stripping dangerous tags and inline event handlers
|
||||
let cleaned = rawHtml
|
||||
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "")
|
||||
.replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, "")
|
||||
.replace(/<object\b[^<]*(?:(?!<\/object>)<[^<]*)*<\/object>/gi, "")
|
||||
.replace(/<embed\b[^<]*(?:(?!<\/embed>)<[^<]*)*<\/embed>/gi, "")
|
||||
.replace(/\s*on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, "");
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
cleaned = DOMPurify.sanitize(cleaned, {
|
||||
ALLOWED_TAGS: [
|
||||
"a", "b", "blockquote", "br", "caption", "code", "div", "em", "h1", "h2", "h3",
|
||||
"h4", "h5", "h6", "hr", "i", "img", "li", "ol", "p", "pre", "span", "strong",
|
||||
"table", "tbody", "td", "tfoot", "th", "thead", "tr", "u", "ul", "style", "font",
|
||||
"sub", "sup"
|
||||
],
|
||||
ALLOWED_ATTR: [
|
||||
"align", "alt", "bgcolor", "border", "cellpadding", "cellspacing", "cite",
|
||||
"class", "color", "colspan", "dir", "height", "href", "id", "lang", "rowspan",
|
||||
"src", "style", "title", "width", "target", "rel", "data-blocked-src"
|
||||
],
|
||||
ALLOW_DATA_ATTR: true,
|
||||
FORBID_TAGS: ["script", "iframe", "object", "embed", "form", "input", "button"],
|
||||
FORBID_ATTR: ["onerror", "onload", "onclick", "onmouseover", "onfocus"],
|
||||
});
|
||||
}
|
||||
|
||||
return DOMPurify.sanitize(rawHtml, {
|
||||
ALLOWED_TAGS: [
|
||||
"a", "b", "blockquote", "br", "caption", "code", "div", "em", "h1", "h2", "h3",
|
||||
"h4", "h5", "h6", "hr", "i", "img", "li", "ol", "p", "pre", "span", "strong",
|
||||
"table", "tbody", "td", "tfoot", "th", "thead", "tr", "u", "ul", "style", "font",
|
||||
"sub", "sup"
|
||||
],
|
||||
ALLOWED_ATTR: [
|
||||
"align", "alt", "bgcolor", "border", "cellpadding", "cellspacing", "cite",
|
||||
"class", "color", "colspan", "dir", "height", "href", "id", "lang", "rowspan",
|
||||
"src", "style", "title", "width", "target", "rel", "data-blocked-src"
|
||||
],
|
||||
ALLOW_DATA_ATTR: true,
|
||||
ADD_ATTR: ["target"],
|
||||
FORBID_TAGS: ["script", "iframe", "object", "embed", "form", "input", "button"],
|
||||
FORBID_ATTR: ["onerror", "onload", "onclick", "onmouseover", "onfocus"],
|
||||
});
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,7 +44,7 @@ export function sanitizeEmailHtml(rawHtml: string): string {
|
||||
export function blockRemoteImages(html: string): string {
|
||||
if (!html) return "";
|
||||
return html
|
||||
.replace(/(<img[^>]+)src\s*=\s*("|')https?:[^"']*\2/gi, "$1data-blocked-src=$2$2")
|
||||
.replace(/(<img\b[^>]*?\b)src\s*=\s*("|')(https?:[^"']*)\2/gi, "$1data-blocked-src=$2$3$2")
|
||||
.replace(/(<[^>]+)background\s*=\s*("|')https?:[^"']*\2/gi, "$1")
|
||||
.replace(/url\(\s*['"]?https?:[^)]*\)/gi, "none");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { sanitizeEmailHtml, blockRemoteImages, unblockRemoteImages } from "./lib/sanitizer.js";
|
||||
|
||||
describe("DOMPurify HTML Sanitizer (Section 14.2)", () => {
|
||||
it("strips malicious script tags and inline event handlers", () => {
|
||||
const maliciousHtml = `<div><script>alert('xss')</script><img src="x" onerror="alert('hack')" /><p>Safe text</p></div>`;
|
||||
const sanitized = sanitizeEmailHtml(maliciousHtml);
|
||||
expect(sanitized).not.toContain("<script>");
|
||||
expect(sanitized).not.toContain("onerror");
|
||||
expect(sanitized).toContain("<p>Safe text</p>");
|
||||
});
|
||||
|
||||
it("strips iframe and object tags", () => {
|
||||
const htmlWithIframe = `<div><iframe src="http://malicious.com"></iframe><object data="test.swf"></object><span>Hello</span></div>`;
|
||||
const sanitized = sanitizeEmailHtml(htmlWithIframe);
|
||||
expect(sanitized).not.toContain("<iframe");
|
||||
expect(sanitized).not.toContain("<object");
|
||||
expect(sanitized).toContain("<span>Hello</span>");
|
||||
});
|
||||
|
||||
it("blocks remote tracking images by default", () => {
|
||||
const html = `<p>Test</p><img src="https://tracker.com/pixel.png" alt="tracker" />`;
|
||||
const sanitized = sanitizeEmailHtml(html);
|
||||
const blocked = blockRemoteImages(sanitized);
|
||||
expect(blocked).toContain('data-blocked-src="https://tracker.com/pixel.png"');
|
||||
expect(blocked).not.toContain(' src="https://tracker.com/pixel.png"');
|
||||
});
|
||||
|
||||
it("unblocks remote images when requested by user", () => {
|
||||
const html = `<p>Test</p><img src="https://tracker.com/pixel.png" alt="tracker" />`;
|
||||
const sanitized = sanitizeEmailHtml(html);
|
||||
const blocked = blockRemoteImages(sanitized);
|
||||
const unblocked = unblockRemoteImages(blocked);
|
||||
expect(unblocked).toContain('src="https://tracker.com/pixel.png"');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user