From 783a205c5846a66fc63b7d38f130a310ab2b672a Mon Sep 17 00:00:00 2001 From: mahula Date: Thu, 15 Jan 2026 13:29:34 +0100 Subject: [PATCH] fix(security): prevent XSS in simpleMarkdownToHtml tag restoration - Add strict regex patterns for tag restoration that only match exact expected format - Add sanitizeUrl() to block javascript:, data:, vbscript: URLs in markdown links - Add containsDangerousAttributes() to detect event handlers in restored content - Prevents onclick/onload injection via malformed preprocessed tags --- .../TipTap/utils/simpleMarkdownToHtml.tsx | 74 ++++++++++++++++--- 1 file changed, 63 insertions(+), 11 deletions(-) diff --git a/lib/src/Components/TipTap/utils/simpleMarkdownToHtml.tsx b/lib/src/Components/TipTap/utils/simpleMarkdownToHtml.tsx index 23cd004c..9e1dff3a 100644 --- a/lib/src/Components/TipTap/utils/simpleMarkdownToHtml.tsx +++ b/lib/src/Components/TipTap/utils/simpleMarkdownToHtml.tsx @@ -3,6 +3,35 @@ import { decodeTag } from '#utils/FormatTags' import type { Item } from '#types/Item' import type { Tag } from '#types/Tag' +/** + * Checks if a string contains potentially dangerous attributes (XSS prevention). + * Returns true if the string contains event handlers or javascript: URLs. + */ +function containsDangerousAttributes(str: string): boolean { + // Check for event handlers (onclick, onload, onerror, onmouseover, etc.) + const eventHandlerPattern = /\bon\w+\s*=/i + // Check for javascript: or data: URLs in attributes + const dangerousUrlPattern = /(?:javascript|data|vbscript):/i + + return eventHandlerPattern.test(str) || dangerousUrlPattern.test(str) +} + +/** + * Sanitizes a URL for safe use in href attributes. + * Returns '#' for dangerous URLs like javascript:, data:, vbscript: + */ +function sanitizeUrl(url: string): string { + const trimmed = url.trim().toLowerCase() + if ( + trimmed.startsWith('javascript:') || + trimmed.startsWith('data:') || + trimmed.startsWith('vbscript:') + ) { + return '#' + } + return url +} + /** * Simple markdown to HTML converter for static rendering. * Handles basic markdown syntax without requiring TipTap. @@ -23,15 +52,37 @@ export function simpleMarkdownToHtml( // Escape HTML first (but preserve our preprocessed tags) html = html.replace(/&/g, '&').replace(//g, '>') - // Restore our preprocessed tags + // Restore our preprocessed tags with STRICT patterns to prevent XSS + // Only restore tags that match exact expected format (no extra attributes allowed) + // After escaping: < becomes <, > becomes >, but " stays as " html = html - .replace(/<video-embed/g, '') - .replace(/<span data-hashtag/g, '') - .replace(/></g, '><') - .replace(/">/g, '">') + // video-embed: only allow provider and video-id attributes + .replace( + /<video-embed provider="(youtube|rumble)" video-id="([^"]+)"><\/video-embed>/g, + (match, provider, videoId) => { + // Validate videoId contains only safe characters + if (!/^[\w-]+$/.test(videoId)) return match + return `` + }, + ) + // hashtag span: only allow data-hashtag and data-label attributes + .replace( + /<span data-hashtag data-label="([^"]+)">(#[^&]+)<\/span>/g, + (match, label, tagText) => { + // Ensure no dangerous content in label + if (containsDangerousAttributes(label)) return match + return `${tagText}` + }, + ) + // item-mention span: only allow data-item-mention, data-label, and data-id attributes + .replace( + /<span data-item-mention data-label="([^"]+)" data-id="([^"]+)">(@[^&]+)<\/span>/g, + (match, label, id, mentionText) => { + // Ensure no dangerous content + if (containsDangerousAttributes(label) || containsDangerousAttributes(id)) return match + return `${mentionText}` + }, + ) // Convert video-embed tags to iframes html = html.replace( @@ -78,11 +129,12 @@ export function simpleMarkdownToHtml( // Inline code: `code` html = html.replace(/`([^`]+)`/g, '$1') - // Links: [text](url) + // Links: [text](url) - with URL sanitization for XSS prevention html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, linkText: string, url: string) => { - const isExternal = url.startsWith('http') + const safeUrl = sanitizeUrl(url) + const isExternal = safeUrl.startsWith('http') const attrs = isExternal ? 'target="_blank" rel="noopener noreferrer"' : '' - return `${linkText}` + return `${linkText}` }) // Headers: # text, ## text, etc.