[object Object]

← back to NationalPaperHangers

Validate admin portfolio uploads by magic bytes, not the spoofable mimetype

a3b0896a8b9f496cabebb949d4df21cfacb4fedc · 2026-05-19 07:53:49 -0700 · Steve

Files touched

Diff

commit a3b0896a8b9f496cabebb949d4df21cfacb4fedc
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue May 19 07:53:49 2026 -0700

    Validate admin portfolio uploads by magic bytes, not the spoofable mimetype
---
 routes/admin.js | 15 ++++++++++++++-
 1 file changed, 14 insertions(+), 1 deletion(-)

diff --git a/routes/admin.js b/routes/admin.js
index 2999412..873df3d 100644
--- a/routes/admin.js
+++ b/routes/admin.js
@@ -8,6 +8,7 @@ const db = require('../lib/db');
 const { requireInstaller, requirePaidTier } = require('../lib/auth');
 const stripe = require('../lib/stripe');
 const marketplace = require('../lib/services/marketplace');
+const mediaSniff = require('../lib/media-sniff');
 const router = express.Router();
 
 router.use(requireInstaller);
@@ -878,13 +879,25 @@ router.post('/uploads', uploadLimiter, (req, res, next) => {
       return res.status(400).json({ ok: false, error: msg });
     }
     if (!req.file) return res.status(400).json({ ok: false, error: 'No file received.' });
+    // Authoritative gate: derive the extension from the file's actual magic
+    // bytes, never the spoofable browser Content-Type. multer's fileFilter
+    // only saw the claimed mimetype — a non-image blob mislabelled image/png
+    // clears that filter but fails here. Portfolio uploads are images only,
+    // so a video brand (.mp4/.mov/.webm) is also rejected.
+    const sniffed = mediaSniff.sniffExt(req.file.buffer);
+    if (!sniffed || mediaSniff.isVideoExt(sniffed)) {
+      return res.status(400).json({
+        ok: false,
+        error: 'File contents are not a supported image (JPG, PNG, WebP, or AVIF).'
+      });
+    }
     try {
       const role = String(req.body.role || 'portfolio').toLowerCase();
       const installerId = req.installer.id;
       const dir = path.join(UPLOAD_ROOT, String(installerId));
       fs.mkdirSync(dir, { recursive: true });
       const hash = crypto.createHash('sha256').update(req.file.buffer).digest('hex').slice(0, 16);
-      const ext = EXT_BY_MIME[req.file.mimetype] || '.bin';
+      const ext = sniffed;
       const fname = `${role}-${hash}${ext}`;
       const fpath = path.join(dir, fname);
       fs.writeFileSync(fpath, req.file.buffer);

← aaf1edb Reject bookings that fall outside the installer's published  ·  back to NationalPaperHangers  ·  Reject malformed emails at signup and claim instead of just 50d107f →