fix: critical SVG XSS vulnerability in file uploads

- Add comprehensive file type validation to prevent SVG uploads
- Update upload widgets to restrict file types at input level
- Enhanced HTML sanitization to block SVG tags and data URLs
- Create centralized file validation utilities with shared constants
- Add client-side and server-side protection against malicious uploads

Security Impact:
- Prevents stored XSS attacks through malicious SVG profile uploads
- Blocks execution of JavaScript embedded in SVG files
- Enforces whitelist of safe image formats (JPEG, PNG, GIF, WebP)
- Provides defense-in-depth protection across all upload components

Fixes reported vulnerability in profile image upload functionality.
This commit is contained in:
Rotimi Best
2025-12-03 11:25:00 +01:00
parent fb2ea885e1
commit 900f5ec129
6 changed files with 216 additions and 9 deletions
@@ -16,7 +16,7 @@ export function sanitizeHtml(html: string): string {
return fallbackSanitize(html);
}
return DOMPurify.sanitize(html, {
let sanitized = DOMPurify.sanitize(html, {
// Allow safe HTML tags
ALLOWED_TAGS: [
'p',
@@ -59,7 +59,7 @@ export function sanitizeHtml(html: string): string {
'onselect',
'onunload'
],
// Forbid dangerous tags
// Forbid dangerous tags (including svg)
FORBID_TAGS: [
'script',
'iframe',
@@ -71,7 +71,9 @@ export function sanitizeHtml(html: string): string {
'select',
'button',
'meta',
'link'
'link',
'svg',
'math'
],
// Keep content of forbidden tags
KEEP_CONTENT: true,
@@ -79,10 +81,15 @@ export function sanitizeHtml(html: string): string {
SANITIZE_DOM: true,
// Allow data attributes
ALLOW_DATA_ATTR: false,
// Transform URLs
// Transform URLs and filter dangerous protocols
ALLOWED_URI_REGEXP:
/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i
});
// Additional post-processing to remove any SVG data URLs that might have slipped through
sanitized = sanitized.replace(/src\s*=\s*["']data:image\/svg[^"']*["']/gi, 'src=""');
return sanitized;
}
/**
@@ -91,12 +98,19 @@ export function sanitizeHtml(html: string): string {
function fallbackSanitize(html: string): string {
if (typeof html !== 'string') return '';
// Remove SVG tags and their content completely
html = html.replace(/<svg\b[^<]*(?:(?!<\/svg>)<[^<]*)*<\/svg>/gi, '');
html = html.replace(/<svg\b[^>]*\/>/gi, '');
// Remove script tags and their content
html = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
// Remove javascript: protocol from attributes
html = html.replace(/javascript:/gi, '');
// Remove data:image/svg URLs which can contain XSS
html = html.replace(/src\s*=\s*["']data:image\/svg[^"']*["']/gi, 'src=""');
// Remove on* event handlers (onclick, onload, onerror, etc.)
html = html.replace(/\s*on\w+\s*=\s*["'][^"']*["']/gi, '');
html = html.replace(/\s*on\w+\s*=\s*[^\s>]*/gi, '');
@@ -120,7 +134,9 @@ function fallbackSanitize(html: string): string {
'input',
'textarea',
'select',
'button'
'button',
'svg',
'math'
];
dangerousTags.forEach((tag) => {
const regex = new RegExp(`<${tag}\\b[^>]*>.*?<\\/${tag}>`, 'gi');