Skip to content

Commit a6f1f45

Browse files
Replace DOMPurify with sanitize-html for server-side HTML sanitization
1 parent 51ed60d commit a6f1f45

5 files changed

Lines changed: 207 additions & 30 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ A comprehensive, production-ready website monitoring and status dashboard built
4040

4141
### Smart Status Detection
4242
- **Working**: Routes with 200-299 responses and >70% uptime
43-
- **Degraded**: Routes with 4xx errors, redirects or bad uptime (<70%)
43+
- **Previous Degradations**: Routes that were previously degraded but are now healthy [Routes with 200-299 responses and <70% uptime]
44+
- **Degraded**: Routes with 4xx errors or redirects too often
4445
- **Broken**: Routes with connection failures (timeout/network) or 5xx server errors
4546
- **Unknown**: Routes with no recent check data
4647

components/project-preview.tsx

Lines changed: 23 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
// noinspection HtmlRequiredLangAttribute
22

33
import {ExternalLink} from "lucide-react"
4-
import {JSDOM} from "jsdom"
5-
import createDOMPurify from "dompurify"
4+
import sanitizeHtml from "sanitize-html"
65

76
interface ProjectPreviewProps {
87
url: string
@@ -30,14 +29,10 @@ export async function ProjectPreview({url, title, renderUrl}: ProjectPreviewProp
3029
if (response.ok) {
3130
let html = await response.text()
3231

33-
// Use DOMPurify for proper HTML sanitization
34-
const window = new JSDOM('').window
35-
const DOMPurify = createDOMPurify(window as any)
36-
37-
// Configure DOMPurify to be strict but allow necessary elements
38-
html = DOMPurify.sanitize(html, {
39-
ALLOWED_TAGS: [
40-
'html', 'head', 'body', 'title', 'meta', 'link', 'style',
32+
// Server-side sanitization using sanitize-html (no jsdom/jsdom parse5 ESM issues)
33+
html = sanitizeHtml(html, {
34+
allowedTags: [
35+
'html', 'head', 'body', 'title', 'meta', 'link',
4136
'div', 'span', 'p', 'a', 'img', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
4237
'ul', 'ol', 'li', 'table', 'thead', 'tbody', 'tr', 'td', 'th',
4338
'header', 'footer', 'nav', 'section', 'article', 'aside', 'main',
@@ -47,25 +42,24 @@ export async function ProjectPreview({url, title, renderUrl}: ProjectPreviewProp
4742
'video', 'audio', 'source', 'track', 'canvas', 'svg', 'path', 'circle',
4843
'rect', 'line', 'polyline', 'polygon', 'g', 'text', 'tspan'
4944
],
50-
ALLOWED_ATTR: [
51-
'href', 'src', 'alt', 'title', 'class', 'id', 'style', 'width', 'height',
52-
'data-*', 'aria-*', 'role', 'rel', 'target', 'type', 'name', 'value',
53-
'placeholder', 'disabled', 'readonly', 'checked', 'selected',
54-
'colspan', 'rowspan', 'cellpadding', 'cellspacing', 'border',
55-
'viewBox', 'd', 'fill', 'stroke', 'stroke-width', 'transform',
56-
'xmlns', 'xmlns:xlink', 'x', 'y', 'cx', 'cy', 'r', 'rx', 'ry',
57-
'x1', 'x2', 'y1', 'y2', 'points'
58-
],
59-
FORBID_TAGS: ['script', 'noscript', 'object', 'embed', 'applet', 'iframe', 'base'],
60-
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onfocus', 'onblur'],
61-
ALLOW_DATA_ATTR: true,
62-
ALLOW_ARIA_ATTR: true,
63-
WHOLE_DOCUMENT: true,
64-
RETURN_DOM: false,
65-
RETURN_DOM_FRAGMENT: false,
66-
SANITIZE_DOM: true,
67-
KEEP_CONTENT: true,
68-
IN_PLACE: false
45+
allowedAttributes: {
46+
'*': [
47+
'href', 'src', 'alt', 'title', 'class', 'id', 'width', 'height',
48+
'role', 'rel', 'target', 'type', 'name', 'value', 'placeholder', 'disabled', 'readonly', 'checked', 'selected',
49+
'colspan', 'rowspan', 'cellpadding', 'cellspacing', 'border',
50+
'viewBox', 'd', 'fill', 'stroke', 'stroke-width', 'transform',
51+
'xmlns', 'xmlns:xlink', 'x', 'y', 'cx', 'cy', 'r', 'rx', 'ry',
52+
'x1', 'x2', 'y1', 'y2', 'points',
53+
'data-*', 'aria-*'
54+
]
55+
},
56+
// Disallow potentially dangerous tags/attributes
57+
disallowedTagsMode: 'discard',
58+
// Allow data and blob URLs for images
59+
allowProtocolRelative: true,
60+
allowedSchemesByTag: {
61+
img: ['http', 'https', 'data', 'blob']
62+
}
6963
})
7064

7165
// Remove existing CSP meta tags that might conflict

package-lock.json

Lines changed: 160 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"react-hook-form": "^7.60.0",
6262
"react-resizable-panels": "^2.1.7",
6363
"recharts": "2.15.4",
64+
"sanitize-html": "^2.17.0",
6465
"sonner": "^1.7.4",
6566
"tailwind-merge": "^3.3.1",
6667
"tailwindcss-animate": "^1.0.7",
@@ -72,6 +73,7 @@
7273
"@types/node": "^22",
7374
"@types/react": "^19",
7475
"@types/react-dom": "^19",
76+
"@types/sanitize-html": "^2.16.0",
7577
"postcss": "^8.5",
7678
"tailwindcss": "^4.1.9",
7779
"tw-animate-css": "1.3.3",

types/sanitize-html.d.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
declare module 'sanitize-html' {
2+
export interface AllowedSchemesByTag {
3+
[tagName: string]: string[]
4+
}
5+
6+
export interface SanitizeHtmlOptions {
7+
allowedTags?: string[]
8+
allowedAttributes?: { [tag: string]: string[] } | string[]
9+
allowedSchemesByTag?: AllowedSchemesByTag
10+
allowedSchemes?: string[]
11+
allowProtocolRelative?: boolean
12+
disallowedTagsMode?: 'discard' | 'escape'
13+
[key: string]: any
14+
}
15+
16+
declare function sanitizeHtml(dirty: string, options?: SanitizeHtmlOptions): string
17+
18+
export default sanitizeHtml
19+
}
20+

0 commit comments

Comments
 (0)