Derive uploaded file extension from mimetype, not originalname (#11)
The multer filename kept path.extname(file.originalname), while the fileFilter only checked the spoofable client-supplied mimetype. An attacker could send Content-Type: image/png with originalname x.html, landing an .html file in /uploads that express.static serves as text/html — same-origin stored XSS. - Store the extension from a whitelist keyed by the accepted mimetype (MIME_EXT), never from originalname. The fileFilter uses the same map as its single source of truth, so only mimetypes with a safe mapped extension pass. - Use crypto.randomBytes for the random filename component. - Serve /uploads with an explicit X-Content-Type-Options: nosniff (defense in depth alongside helmet's global setting). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -51,8 +51,16 @@ const UPLOAD_DIR = process.env.UPLOAD_DIR || path.join(SERVER_ROOT, 'uploads')
|
||||
const CLIENT_DIST = path.join(REPO_ROOT, 'client', 'dist')
|
||||
fs.mkdirSync(UPLOAD_DIR, { recursive: true })
|
||||
|
||||
// Uploaded images — always served, even during maintenance.
|
||||
app.use('/uploads', express.static(UPLOAD_DIR))
|
||||
// Uploaded images — always served, even during maintenance. Force nosniff so a
|
||||
// stored file is never interpreted as anything other than its declared type
|
||||
// (defense in depth alongside helmet's global X-Content-Type-Options, and in
|
||||
// case that global config is ever changed).
|
||||
app.use(
|
||||
'/uploads',
|
||||
express.static(UPLOAD_DIR, {
|
||||
setHeaders: (res) => res.set('X-Content-Type-Options', 'nosniff'),
|
||||
}),
|
||||
)
|
||||
|
||||
// ── API ───────────────────────────────────────────────────────────────
|
||||
app.get('/api/health', (req, res) => res.json({ status: 'ok' }))
|
||||
|
||||
Reference in New Issue
Block a user