filter with dompurify
This commit is contained in:
Generated
+590
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user