Description
Accepting uploads without checking allowed extensions, MIME types and file signatures can store malicious files on the server.
Potential impact
- Executable files or scripts may be stored in the server's filesystem.
- Files placed in a publicly served directory may deliver malicious content to users.
Remediation
- Validate extensions, MIME types and file signatures against an allow-list.
- Generate filenames on the server and keep upload storage separate from executable paths.
- Combine
multer'sfileFilterwith a separate content-validation layer.
Examples
The Express app and quarantine directory must already exist. The first example assumes upload has no file-type validation.
Before
javascript
app.post("/upload", upload.single("file"), (req, res) => {
res.json({ path: req.file.path });
});
After
javascript
const crypto = require("crypto");
const fs = require("fs/promises");
const path = require("path");
const multer = require("multer");
// An isolated directory, not public or executable, with writes restricted to the application
const QUARANTINE_DIR = "/srv/app/upload-quarantine";
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const upload = multer({
storage: multer.diskStorage({
destination: QUARANTINE_DIR,
filename(req, file, cb) {
cb(null, `${crypto.randomUUID()}.png`);
},
}),
limits: { fileSize: 5 * 1024 * 1024 },
fileFilter(req, file, cb) {
const extension = path.extname(file.originalname).toLowerCase();
const allowed = file.mimetype === "image/png" && extension === ".png";
cb(null, allowed);
},
});
async function hasPngSignature(filePath) {
const handle = await fs.open(filePath, "r");
try {
const header = Buffer.alloc(PNG_SIGNATURE.length);
const { bytesRead } = await handle.read(header, 0, header.length, 0);
return bytesRead === header.length && header.equals(PNG_SIGNATURE);
} finally {
await handle.close();
}
}
app.post("/upload", upload.single("file"), async (req, res, next) => {
if (!req.file) return res.status(400).send("invalid file type");
try {
if (!(await hasPngSignature(req.file.path))) {
await fs.unlink(req.file.path);
return res.status(400).send("invalid file type");
}
return res.json({ id: req.file.filename });
} catch (error) {
await fs.unlink(req.file.path).catch(() => {});
return next(error);
}
});
Explanation:
- Before: Unvalidated uploads may store malicious scripts or executable files.
- After: The handler checks size, extension, MIME type and the PNG signature, using a generated name in private quarantine storage. It handles a missing
req.filewhen the filter rejects a file and deletes temporary files on content-validation failure or error. A PNG signature alone does not establish that the entire file is valid or harmless. Add purpose-specific checks, such as image decoding and re-encoding, where needed.