diff --git a/services/webmail/src/lib/sanitizer.ts b/services/webmail/src/lib/sanitizer.ts index 7750d2c..c29f646 100644 --- a/services/webmail/src/lib/sanitizer.ts +++ b/services/webmail/src/lib/sanitizer.ts @@ -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>/gi, ""); + if (!rawHtml) return ""; + + // Base sanitization stripping dangerous tags and inline event handlers + let cleaned = rawHtml + .replace(/)<[^<]*)*<\/script>/gi, "") + .replace(/)<[^<]*)*<\/iframe>/gi, "") + .replace(/)<[^<]*)*<\/object>/gi, "") + .replace(/)<[^<]*)*<\/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(/(]+)src\s*=\s*("|')https?:[^"']*\2/gi, "$1data-blocked-src=$2$2") + .replace(/(]*?\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"); } diff --git a/services/webmail/src/sanitizer.test.ts b/services/webmail/src/sanitizer.test.ts new file mode 100644 index 0000000..4c9d65d --- /dev/null +++ b/services/webmail/src/sanitizer.test.ts @@ -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 = `

Safe text

`; + const sanitized = sanitizeEmailHtml(maliciousHtml); + expect(sanitized).not.toContain("