From 4cda9c8c0adad49e8179c86ffa69b9685b642608 Mon Sep 17 00:00:00 2001 From: Niklas Date: Sat, 5 Sep 2026 13:02:50 +0200 Subject: [PATCH] fix(upload): store sniffed MIME instead of generic CDN content-type --- backend/routes/upload.js | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/backend/routes/upload.js b/backend/routes/upload.js index 08e497c..42084ef 100644 --- a/backend/routes/upload.js +++ b/backend/routes/upload.js @@ -326,15 +326,18 @@ function downloadImage(imageUrl, maxRedirects = 5) { }); response.on('end', () => { + const buffer = Buffer.concat(chunks); + let resolvedType = contentType; if (typeIsGeneric) { - const sniffed = sniffMime(Buffer.concat(chunks)); - if (!sniffed) { + // Replace the generic CDN type with what the bytes actually are, + // so the correct MIME gets stored and served. + resolvedType = sniffMimeExact(buffer); + if (!resolvedType) { return reject(new Error('Unsupported content type: binary/octet-stream (unknown magic bytes)')); } } - const buffer = Buffer.concat(chunks); const filename = parsed.pathname.split('/').pop() || 'image'; - resolve({ buffer, mimeType: contentType, filename }); + resolve({ buffer, mimeType: resolvedType, filename }); }); response.on('error', reject); @@ -435,22 +438,22 @@ router.use((err, req, res, next) => { // Sniff the real MIME type from magic bytes (for CDNs that serve everything -// as binary/octet-stream). Returns true when the buffer looks like an -// allowed image/video/pdf type. -function sniffMime(buf) { - if (!buf || buf.length < 12) return false; +// as binary/octet-stream). Returns the exact MIME or null. +function sniffMimeExact(buf) { + if (!buf || buf.length < 12) return null; const hex = buf.slice(0, 12).toString('hex'); const ascii = buf.slice(0, 12).toString('latin1'); - if (hex.startsWith('89504e47')) return true; // PNG - if (hex.startsWith('ffd8ff')) return true; // JPEG - if (ascii.startsWith('GIF87a') || ascii.startsWith('GIF89a')) return true; // GIF - if (ascii.startsWith('RIFF') && buf.slice(8, 12).toString('latin1') === 'WEBP') return true; // WebP - if (ascii.startsWith('%PDF')) return true; // PDF - if (ascii.startsWith('ftyp')) return true; // MP4/QuickTime (ftyp at 0 for some, 4 for others) - if (buf.slice(4, 8).toString('latin1') === 'ftyp') return true; - if (hex.startsWith('1a45dfa3')) return true; // WebM/Matroska - if (ascii.startsWith('OggS')) return false; // ogg not allowed - return false; + if (hex.startsWith('89504e47')) return 'image/png'; + if (hex.startsWith('ffd8ff')) return 'image/jpeg'; + if (ascii.startsWith('GIF87a') || ascii.startsWith('GIF89a')) return 'image/gif'; + if (ascii.startsWith('RIFF') && buf.slice(8, 12).toString('latin1') === 'WEBP') return 'image/webp'; + if (ascii.startsWith('%PDF')) return 'application/pdf'; + if (buf.slice(4, 8).toString('latin1') === 'ftyp') { + const brand = buf.slice(8, 12).toString('latin1'); + return brand.startsWith('qt') ? 'video/quicktime' : 'video/mp4'; + } + if (hex.startsWith('1a45dfa3')) return 'video/webm'; + return null; } module.exports = router;