Missing upload file-type validation

Missing upload file-type validation

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's fileFilter with 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.file when 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.

References