fix(app): replace custom HTML sanitizer with DOMPurify
Install dompurify and replace the hand-rolled DOM walker sanitizer with DOMPurify.sanitize() configured with the same allowed tags. Handles mutation XSS edge cases the custom version couldn't. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
f7f8c140c3
commit
4053eb46af
@@ -33,7 +33,8 @@
|
||||
"plyr": "^3.8.4",
|
||||
"vue": "^3.5.29",
|
||||
"vue-router": "^5.0.3",
|
||||
"wavesurfer.js": "^7.12.1"
|
||||
"wavesurfer.js": "^7.12.1",
|
||||
"dompurify": "^3.2.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
|
||||
@@ -13,33 +13,16 @@ export function isSafeUrl(u: string | undefined): u is string {
|
||||
return !!u && typeof u === 'string' && /^https?:\/\//i.test(u.trim())
|
||||
}
|
||||
|
||||
const ALLOWED_TAGS = new Set(['p', 'br', 'a', 'strong', 'em', 'b', 'i', 'ul', 'ol', 'li', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'span', 'div'])
|
||||
const DANGEROUS_TAGS = new Set(['script', 'style', 'iframe', 'object', 'embed'])
|
||||
import DOMPurify from 'dompurify'
|
||||
|
||||
/** Sanitize HTML: allow safe tags, strip scripts and dangerous attributes */
|
||||
const ALLOWED_TAGS_LIST = ['p', 'br', 'a', 'strong', 'em', 'b', 'i', 'ul', 'ol', 'li', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'span', 'div']
|
||||
|
||||
/** Sanitize HTML using DOMPurify with restricted tag set */
|
||||
export function sanitizeHtml(html: string): string {
|
||||
const div = document.createElement('div')
|
||||
div.innerHTML = html
|
||||
const walk = (node: Node): string => {
|
||||
if (node.nodeType === Node.TEXT_NODE) return node.textContent ?? ''
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return ''
|
||||
const el = node as Element
|
||||
const tag = el.tagName.toLowerCase()
|
||||
if (DANGEROUS_TAGS.has(tag)) return ''
|
||||
if (!ALLOWED_TAGS.has(tag)) return [...node.childNodes].map(walk).join('')
|
||||
const attrs: string[] = []
|
||||
if (tag === 'a' && el.getAttribute('href')) {
|
||||
const href = el.getAttribute('href') ?? ''
|
||||
if (/^https?:\/\//i.test(href) && !/javascript:/i.test(href)) attrs.push(`href="${href.replace(/"/g, '"')}"`)
|
||||
}
|
||||
if (tag === 'img' && el.getAttribute('src')) {
|
||||
const src = el.getAttribute('src') ?? ''
|
||||
if (/^https?:\/\//i.test(src)) attrs.push(`src="${src.replace(/"/g, '"')}"`)
|
||||
}
|
||||
const inner = [...node.childNodes].map(walk).join('')
|
||||
return `<${tag}${attrs.length ? ' ' + attrs.join(' ') : ''}>${inner}</${tag}>`
|
||||
}
|
||||
return [...div.childNodes].map(walk).join('')
|
||||
return DOMPurify.sanitize(html, {
|
||||
ALLOWED_TAGS: ALLOWED_TAGS_LIST,
|
||||
ALLOWED_ATTR: ['href', 'src', 'target', 'rel'],
|
||||
})
|
||||
}
|
||||
|
||||
/** Escape HTML entities for safe rendering in a text context */
|
||||
|
||||
Generated
+3
@@ -27,6 +27,9 @@ importers:
|
||||
'@tanstack/vue-virtual':
|
||||
specifier: ^3.13.19
|
||||
version: 3.13.19(vue@3.5.29(typescript@5.8.3))
|
||||
dompurify:
|
||||
specifier: ^3.2.6
|
||||
version: 3.3.1
|
||||
hls.js:
|
||||
specifier: ^1.6.15
|
||||
version: 1.6.15
|
||||
|
||||
Reference in New Issue
Block a user