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
@@ -3,6 +3,7 @@
import { Loading } from 'carbon-components-svelte';
import Camera from 'carbon-icons-svelte/lib/Camera.svelte';
import { createEventDispatcher } from 'svelte';
import { validateImageUpload } from '$lib/utils/functions/fileValidation';
export let avatar: string | undefined;
export let src: string | undefined;
@@ -19,13 +20,24 @@
const onFileSelected = (e: Event<HTMLInputElement>) => {
const image = e.target.files[0];
// Validate file type first (security check)
const validation = validateImageUpload(image);
if (!validation.isValid) {
errorMessage = validation.error;
fileinput.value = ''; // Clear the input
return;
}
const maxFileSize = maxFileSizeInMb * 1024 * 1024;
if (image.size > maxFileSize) {
errorMessage = `${$t('settings.profile.profile_picture.validation_error')} ${
maxFileSize / (1024 * 1024)
} MB`;
fileinput.value = ''; // Clear the input
return;
}
let reader = new FileReader();
reader.readAsDataURL(image);
reader.onload = (e) => {
@@ -9,6 +9,7 @@
import { queryUnsplash } from './utils';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { t } from '$lib/utils/functions/translations';
import { validateImageUpload } from '$lib/utils/functions/fileValidation';
export let imageURL = '';
@@ -47,6 +48,16 @@
const onFileSelected = () => {
const file = fileInput?.files?.[0];
if (!file) return;
const validation = validateImageUpload(file);
if (!validation.isValid) {
snackbar.error(validation.error || 'Invalid file type');
label = $t('snackbar.landing_page_settings.error.try_again');
fileInput.value = '';
return;
}
const sizeInkb = file?.size! / 1024;
if (sizeInkb > 500) {
snackbar.error('snackbar.landing_page_settings.error.file_size');
@@ -115,6 +126,7 @@
<div class="w-full">
<input
type="file"
accept=".jpg,.jpeg,.png,.gif,.webp,image/jpeg,image/png,image/gif,image/webp"
style="display: none;"
bind:this={fileInput}
on:change={onFileSelected}