We're sorry but this app doesn't work properly without JavaScript enabled. Please enable it to continue.

This lesson's interactive features are locked, please to keep using them

File Upload Security

File uploads are one of the highest-risk features in web applications. They accept large amounts of user-controlled data in complex formats that may later be executed, parsed, or served. Attackers love to target upload handlers aggressively.

Trusting Upload Metadata

An unsafe handler trusts what the client tells it:

const upload = multer({ storage: multer.memoryStorage() });

app.post(
  "/upload",
  upload.single("file"),
  async (req: Request, res: Response) => {
    const file = req.file;
    if (!file) return res.status(400).json({ error: "No file" });

    await writeFile(`./public/uploads/${file.originalname}`, file.buffer);
    res.status(201).json({ message: "Uploaded" });
  },
);

It writes to an attacker-controlled filename and stores the uninspected contents into a public directory. That means an attacker can upload a dangerous shell.js script renamed to image.png and place it somewhere the application or a browser may actually execute it.

You might think file.mimetype gives you the real content type, but it does not – libraries like multer simply read mimetype from the Content-Type header, which is trivially spoofable by the client.

Detect Before Storing

A safer handler generates its own filename, inspects the file's actual bytes, and stores the file outside any public paths:

const upload = multer({
  storage: multer.memoryStorage(),
  limits: { fileSize: 2 * 1024 * 1024 },
});

const ALLOWED_TYPES = new Set(["image/png", "image/jpeg"]);

app.post(
  "/upload",
  upload.single("file"),
  async (req: Request, res: Response) => {
    const file = req.file;
    if (!file) return res.status(400).json({ error: "No file" });

    const detected = await fileTypeFromBuffer(file.buffer);
    if (!detected || !ALLOWED_TYPES.has(detected.mime)) {
      return res.status(400).json({ error: "Invalid file type" });
    }

    const safeName = `${randomUUID()}.${detected.ext}`;
    await writeFile(`./private/uploads/${safeName}`, file.buffer);
    res.status(201).json({ message: "Uploaded", id: safeName });
  },
);

With file uploads, you should enforce:

  • Size limit: prevents resource exhaustion from oversized uploads
  • File-signature inspection: identifies recognized formats from their bytes instead of trusting client-provided metadata
  • Server-generated filename: keeps an attacker-controlled name out of the storage path, preventing filename traversal. Using a UUID makes collisions impractical.
  • Private storage: keeps uploads outside the app's static-file paths, so they aren't exposed directly as public, trusted assets

See OWASP's full File Upload Cheat Sheet for a full defense-in-depth approach.

Assignment

Bearly Secure already receives tax-document bytes in memory and persists their metadata, but storeTaxDocument accepts any bytes and gives every file a generic name. Detect each file's type before persistence and generate a safe storage name.

With Bearly Secure still running, run and submit the CLI tests from the project root.