From 1dc423a1c498afb8ca268f5b4efe006608b7024f Mon Sep 17 00:00:00 2001 From: Dries Augustyns Date: Tue, 10 Mar 2026 20:10:13 +0100 Subject: [PATCH] chore: Enhance image upload validation with magic byte checks --- apps/api/src/controllers/Uploads.ts | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/apps/api/src/controllers/Uploads.ts b/apps/api/src/controllers/Uploads.ts index d0ff856..3c70e37 100644 --- a/apps/api/src/controllers/Uploads.ts +++ b/apps/api/src/controllers/Uploads.ts @@ -6,6 +6,20 @@ import {requireAuth, requireEmailVerified} from '../middleware/auth.js'; import * as S3Service from '../services/S3Service.js'; import {CatchAsync} from '../utils/asyncHandler.js'; +const MAGIC_BYTES: Record = { + 'image/jpeg': [Buffer.from([0xff, 0xd8, 0xff])], + 'image/jpg': [Buffer.from([0xff, 0xd8, 0xff])], + 'image/png': [Buffer.from([0x89, 0x50, 0x4e, 0x47])], + 'image/gif': [Buffer.from('GIF87a'), Buffer.from('GIF89a')], + 'image/webp': [Buffer.from('RIFF')], +}; + +function validateMagicBytes(buffer: Buffer, mimetype: string): boolean { + const signatures = MAGIC_BYTES[mimetype]; + if (!signatures) return false; + return signatures.some(sig => buffer.subarray(0, sig.length).equals(sig)); +} + // Configure multer for file uploads (memory storage) const upload = multer({ storage: multer.memoryStorage(), @@ -13,12 +27,12 @@ const upload = multer({ fileSize: 10 * 1024 * 1024, // 10MB max file size }, fileFilter: (_req, file, cb) => { - const allowedMimeTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml']; + const allowedMimeTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']; if (allowedMimeTypes.includes(file.mimetype)) { cb(null, true); } else { - cb(new Error('Only image files are allowed (JPEG, PNG, GIF, WebP, SVG)')); + cb(new Error('Only image files are allowed (JPEG, PNG, GIF, WebP)')); } }, }); @@ -48,6 +62,12 @@ export class Uploads { }); } + if (!validateMagicBytes(req.file.buffer, req.file.mimetype)) { + return res.status(400).json({ + error: 'File contents do not match the declared image type', + }); + } + // Upload file to S3/Minio const result = await S3Service.uploadFile({ file: req.file.buffer,