filter with dompurify

This commit is contained in:
Zimeng Xiong
2025-11-22 21:21:28 -08:00
parent 06f13d1404
commit b47ab76785
3 changed files with 788 additions and 28 deletions
+590
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -14,13 +14,16 @@
"dependencies": { "dependencies": {
"@prisma/client": "^5.22.0", "@prisma/client": "^5.22.0",
"@types/archiver": "^7.0.0", "@types/archiver": "^7.0.0",
"@types/jsdom": "^27.0.0",
"@types/multer": "^2.0.0", "@types/multer": "^2.0.0",
"@types/socket.io": "^3.0.1", "@types/socket.io": "^3.0.1",
"archiver": "^7.0.1", "archiver": "^7.0.1",
"better-sqlite3": "^12.4.6", "better-sqlite3": "^12.4.6",
"cors": "^2.8.5", "cors": "^2.8.5",
"dompurify": "^3.3.0",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"express": "^5.1.0", "express": "^5.1.0",
"jsdom": "^27.2.0",
"multer": "^2.0.2", "multer": "^2.0.2",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"zod": "^4.1.12" "zod": "^4.1.12"
+195 -28
View File
@@ -3,50 +3,166 @@
*/ */
import { z } from "zod"; import { z } from "zod";
import DOMPurify from "dompurify";
import { JSDOM } from "jsdom";
// Create a DOM environment for DOMPurify (Node.js compatibility)
const window = new JSDOM("").window;
const purify = DOMPurify(window);
/** /**
* Sanitize HTML/JS content by removing dangerous patterns * Sanitize HTML/JS content using DOMPurify (battle-tested library)
*/ */
export const sanitizeHtml = (input: string): string => { export const sanitizeHtml = (input: string): string => {
if (typeof input !== "string") return ""; if (typeof input !== "string") return "";
return input return purify
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "") // Remove script tags .sanitize(input, {
.replace(/javascript:/gi, "") // Remove javascript: URIs ALLOWED_TAGS: [
.replace(/on\w+\s*=\s*["'][^"']*["']/gi, "") // Remove event handlers // Allow basic text formatting that might be in drawings
.replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, "") // Remove iframes "b",
.replace(/<object\b[^<]*(?:(?!<\/object>)<[^<]*)*<\/object>/gi, "") // Remove objects "i",
.replace(/<embed\b[^<]*(?:(?!<\/embed>)<[^<]*)*<\/embed>/gi, "") // Remove embeds "u",
.replace(/<link\b[^>]*>/gi, "") // Remove link tags "em",
.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, "") // Remove style tags "strong",
"p",
"br",
"span",
"div",
],
ALLOWED_ATTR: [], // No attributes allowed by default for security
FORBID_TAGS: [
// Explicitly forbid dangerous tags
"script",
"iframe",
"object",
"embed",
"link",
"style",
"form",
"input",
"button",
"select",
"textarea",
"svg",
"foreignObject",
],
FORBID_ATTR: [
// Explicitly forbid dangerous attributes
"onload",
"onclick",
"onerror",
"onmouseover",
"onfocus",
"onblur",
"onchange",
"onsubmit",
"onreset",
"onkeydown",
"onkeyup",
"onkeypress",
"href",
"src",
"action",
"formaction",
],
KEEP_CONTENT: true, // Keep content even if tags are removed
})
.trim(); .trim();
}; };
/** /**
* Sanitize SVG content specifically * Sanitize SVG content using DOMPurify with strict SVG restrictions
*/ */
export const sanitizeSvg = (svgContent: string): string => { export const sanitizeSvg = (svgContent: string): string => {
if (typeof svgContent !== "string") return ""; if (typeof svgContent !== "string") return "";
// Remove potentially dangerous SVG elements and attributes // For SVG content, we'll be very restrictive since SVG can execute JavaScript
return svgContent // We only allow basic geometric shapes without any scripts or external references
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "") return purify
.replace(/javascript:/gi, "") .sanitize(svgContent, {
.replace(/on\w+\s*=\s*["'][^"']*["']/gi, "") ALLOWED_TAGS: [
.replace( // Allow only safe SVG geometric elements
/<foreignObject\b[^<]*(?:(?!<\/foreignObject>)<[^<]*)*<\/foreignObject>/gi, "svg",
"" "g",
) "rect",
.replace(/\shref\s*=\s*["'][^"']*(?:javascript:)[^"']*["']/gi, ' href="#"') "circle",
.replace( "ellipse",
/\sxlink:href\s*=\s*["'][^"']*(?:javascript:)[^"']*["']/gi, "line",
' xlink:href="#"' "polyline",
) "polygon",
"path",
"text",
"tspan",
],
ALLOWED_ATTR: [
// Allow only safe geometric attributes
"x",
"y",
"width",
"height",
"cx",
"cy",
"r",
"rx",
"ry",
"x1",
"y1",
"x2",
"y2",
"points",
"d",
"fill",
"stroke",
"stroke-width",
"opacity",
"transform",
"font-size",
"font-family",
"text-anchor",
"dominant-baseline",
],
FORBID_TAGS: [
// Completely forbid any script-related or external content
"script",
"foreignObject",
"iframe",
"object",
"embed",
"use",
"image",
"style",
"link",
"defs",
"symbol",
"marker",
"clipPath",
"mask",
"filter",
],
FORBID_ATTR: [
// Forbid any attributes that could execute code or load external content
"onload",
"onclick",
"onerror",
"onmouseover",
"onfocus",
"onblur",
"href",
"xlink:href",
"src",
"action",
"style",
"class",
"id",
],
KEEP_CONTENT: true,
})
.trim(); .trim();
}; };
/** /**
* Validate and sanitize text content * Validate and sanitize text content using DOMPurify
*/ */
export const sanitizeText = ( export const sanitizeText = (
input: unknown, input: unknown,
@@ -60,8 +176,59 @@ export const sanitizeText = (
// Truncate if too long // Truncate if too long
const truncated = cleaned.slice(0, maxLength); const truncated = cleaned.slice(0, maxLength);
// Final HTML sanitization // Use DOMPurify for text content - more permissive than HTML but still safe
return sanitizeHtml(truncated); return purify
.sanitize(truncated, {
ALLOWED_TAGS: [
// Allow basic text formatting that might be in drawing text
"b",
"i",
"u",
"em",
"strong",
"br",
"span",
],
ALLOWED_ATTR: [], // No attributes allowed for text content
FORBID_TAGS: [
// Block potentially dangerous tags
"script",
"iframe",
"object",
"embed",
"link",
"style",
"form",
"input",
"button",
"select",
"textarea",
"svg",
"foreignObject",
],
FORBID_ATTR: [
// Block all event handlers and dangerous attributes
"onload",
"onclick",
"onerror",
"onmouseover",
"onfocus",
"onblur",
"onchange",
"onsubmit",
"onreset",
"onkeydown",
"onkeyup",
"onkeypress",
"href",
"src",
"action",
"formaction",
"style",
],
KEEP_CONTENT: true,
})
.trim();
}; };
/** /**