security: sanitiser la sortie marked avec DOMPurify (XSS)
export-pdf-button et print-button injectaient marked(content) directement dans innerHTML / document.write. Un lien Markdown javascript: passait le filtre hasRestrictedChar et pouvait s'exécuter. Ajout de DOMPurify.sanitize() via import dynamique (déjà présent en dep transitive de jspdf) sur les deux composants, avec whitelist de tags et d'attributs stricte. markdown-renderer n'est pas touché car react-markdown-preview utilise rehype-sanitize en interne. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -29,8 +29,11 @@ const renderMarkdownToHtml = async content => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Dynamic import of markdown parser
|
||||
const {marked} = await import('marked')
|
||||
// Dynamic import of markdown parser and sanitizer
|
||||
const [{marked}, {default: DOMPurify}] = await Promise.all([
|
||||
import('marked'),
|
||||
import('dompurify')
|
||||
])
|
||||
// Configure marked for better PDF rendering
|
||||
marked.setOptions({
|
||||
breaks: true, // Convert \n to <br>
|
||||
@@ -39,7 +42,42 @@ const renderMarkdownToHtml = async content => {
|
||||
mangle: false // Don't mangle email addresses
|
||||
})
|
||||
|
||||
return marked(content)
|
||||
return DOMPurify.sanitize(marked(content), {
|
||||
ALLOWED_TAGS: [
|
||||
'p',
|
||||
'strong',
|
||||
'em',
|
||||
'b',
|
||||
'i',
|
||||
'ul',
|
||||
'ol',
|
||||
'li',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'blockquote',
|
||||
'code',
|
||||
'pre',
|
||||
'br',
|
||||
'hr',
|
||||
'a',
|
||||
'table',
|
||||
'thead',
|
||||
'tbody',
|
||||
'tr',
|
||||
'th',
|
||||
'td',
|
||||
],
|
||||
ALLOWED_ATTR: [
|
||||
'href',
|
||||
'target',
|
||||
'rel',
|
||||
],
|
||||
ALLOW_DATA_ATTR: false,
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse markdown, falling back to plain text:', error)
|
||||
return content.replaceAll('\n', '<br>')
|
||||
|
||||
@@ -31,8 +31,11 @@ const renderMarkdownToHtml = async content => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Dynamic import of markdown parser
|
||||
const {marked} = await import('marked')
|
||||
// Dynamic import of markdown parser and sanitizer
|
||||
const [{marked}, {default: DOMPurify}] = await Promise.all([
|
||||
import('marked'),
|
||||
import('dompurify')
|
||||
])
|
||||
|
||||
// Configure marked for better print rendering
|
||||
marked.setOptions({
|
||||
@@ -42,7 +45,42 @@ const renderMarkdownToHtml = async content => {
|
||||
mangle: false // Don't mangle email addresses
|
||||
})
|
||||
|
||||
return marked(content)
|
||||
return DOMPurify.sanitize(marked(content), {
|
||||
ALLOWED_TAGS: [
|
||||
'p',
|
||||
'strong',
|
||||
'em',
|
||||
'b',
|
||||
'i',
|
||||
'ul',
|
||||
'ol',
|
||||
'li',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'blockquote',
|
||||
'code',
|
||||
'pre',
|
||||
'br',
|
||||
'hr',
|
||||
'a',
|
||||
'table',
|
||||
'thead',
|
||||
'tbody',
|
||||
'tr',
|
||||
'th',
|
||||
'td',
|
||||
],
|
||||
ALLOWED_ATTR: [
|
||||
'href',
|
||||
'target',
|
||||
'rel',
|
||||
],
|
||||
ALLOW_DATA_ATTR: false,
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse markdown, falling back to plain text:', error)
|
||||
return content.replaceAll('\n', '<br>')
|
||||
|
||||
Reference in New Issue
Block a user