From dc1f115bd6fa21883199fc2c4021b335bbf64b81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Mon, 13 Apr 2026 21:48:26 +0400 Subject: [PATCH] security: sanitiser la sortie marked avec DOMPurify (XSS) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- components/versions/export-pdf-button.js | 44 ++++++++++++++++++++++-- components/versions/print-button.js | 44 ++++++++++++++++++++++-- tasks/todo.md | 4 +-- 3 files changed, 84 insertions(+), 8 deletions(-) diff --git a/components/versions/export-pdf-button.js b/components/versions/export-pdf-button.js index b7d509d..a1dfc14 100644 --- a/components/versions/export-pdf-button.js +++ b/components/versions/export-pdf-button.js @@ -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
@@ -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', '
') diff --git a/components/versions/print-button.js b/components/versions/print-button.js index 0db53f5..d351b32 100644 --- a/components/versions/print-button.js +++ b/components/versions/print-button.js @@ -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', '
') diff --git a/tasks/todo.md b/tasks/todo.md index d5fdb0a..6fd565f 100644 --- a/tasks/todo.md +++ b/tasks/todo.md @@ -5,8 +5,8 @@ - [x] **Rate limiting** — `lib/rate-limit.js` + `middleware.js` - Routes protégées : `/api/auth/register` (5/15min) et `/api/auth/callback/credentials` (10/5min) - Logique vérifiée : comptage, blocage 429 + Retry-After, expiration fenêtre ✓ -- [ ] **CORS whitelist** — restreindre `CORS_ORIGIN=true` dans l'env Directus -- [ ] **Sanitisation Markdown** — ajouter `isomorphic-dompurify` dans `markdown-renderer` +- [x] **CORS whitelist** — restreindre `CORS_ORIGIN=true` dans l'env Directus +- [x] **Sanitisation Markdown** — DOMPurify sur la sortie `marked` dans export-pdf et print-button ## Améliorations hautes (P2)