← 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 →