From f2e2302c275768921413562535f06ede0b600158 Mon Sep 17 00:00:00 2001 From: The Joel Date: Tue, 25 Aug 2026 23:21:33 +0100 Subject: [PATCH] fix: sanitize HTML in MarkdownRenderer during SSR Remove SSR bypass that returned unsanitized HTML. DOMPurify now runs isomorphically on both server and client to prevent XSS attacks. - Remove typeof window check in MarkdownRenderer - Add tests verifying DOMPurify sanitization --- .../shared/MarkdownRenderer.test.ts | 79 +++++++++++++++++++ src/components/shared/MarkdownRenderer.tsx | 1 - 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/components/shared/MarkdownRenderer.test.ts b/src/components/shared/MarkdownRenderer.test.ts index 9f677175..b1b5af9a 100644 --- a/src/components/shared/MarkdownRenderer.test.ts +++ b/src/components/shared/MarkdownRenderer.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from 'vitest'; import { markdownToHtml } from './MarkdownRenderer'; +import DOMPurify from 'dompurify'; // Tests cover the pure markdownToHtml function (no DOM/React needed). @@ -194,4 +195,82 @@ describe('markdownToHtml', () => { expect(html).toContain('A'); expect(html).toContain('1'); }); + + // ── DOMPurify sanitization ─────────────────────────────────────────────────── + + it('sanitizes malicious HTML with DOMPurify using component config', () => { + const md = ''; + const raw = markdownToHtml(md); + const sanitized = DOMPurify.sanitize(raw, { + ALLOWED_TAGS: [ + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'p', + 'br', + 'strong', + 'em', + 'del', + 'code', + 'pre', + 'ul', + 'ol', + 'li', + 'blockquote', + 'hr', + 'a', + 'img', + 'table', + 'thead', + 'tbody', + 'tr', + 'th', + 'td', + 'input', + ], + ALLOWED_ATTR: ['href', 'src', 'alt', 'class', 'target', 'rel', 'type', 'checked', 'disabled'], + }); + expect(sanitized).not.toContain('onerror'); + }); + + it('sanitizes script tags with DOMPurify using component config', () => { + const md = ''; + const raw = markdownToHtml(md); + const sanitized = DOMPurify.sanitize(raw, { + ALLOWED_TAGS: [ + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'p', + 'br', + 'strong', + 'em', + 'del', + 'code', + 'pre', + 'ul', + 'ol', + 'li', + 'blockquote', + 'hr', + 'a', + 'img', + 'table', + 'thead', + 'tbody', + 'tr', + 'th', + 'td', + 'input', + ], + ALLOWED_ATTR: ['href', 'src', 'alt', 'class', 'target', 'rel', 'type', 'checked', 'disabled'], + }); + expect(sanitized).not.toContain('