File Upload Security

Intermediate
11 min

File Upload Security

An upload endpoint accepts arbitrary bytes from the internet and writes them to disk. Uploads have been used to plant server-side scripts, overwrite configuration files, host malware, and inject scripts into other users' browsers. A handful of rules closes almost every one of those doors.

In this lesson you will learn what can go wrong with uploads, how to validate a file by its content rather than its name, where to store it, and how to serve it back without creating a new vulnerability.

What Can Go Wrong

| Threat | How it works | Primary control | |---|---|---| | Server-side code execution | An uploaded .php or .jsp file is placed where the web server executes it | Never store uploads inside a directory the server executes from | | Path traversal | A filename like ../../config.env overwrites files | Generate the filename on the server | | Stored XSS | An SVG or HTML file containing script is served from your origin | Serve as attachment, from a separate origin, with a strict content type | | Denial of service | Huge files, thousands of files, or archives that expand enormously | Size and count limits, no automatic extraction |

Validate the Content, Not the Name

The filename and the Content-Type header both come from the client and can say anything (invoice.pdf.exe and .PhP have defeated many naive checks). The only reliable signal is the file's bytes. The sample code uses the file-type package to read the magic bytes that identify a format and compares the result with an allowlist of MIME types. Three further rules:

  • Enforce a size limit at the parser (limits.fileSize in multer) so oversized uploads are rejected early.
  • Limit the number of files per request and per user per day.
  • Reject anything the detector cannot identify. Plain text and CSV have no magic bytes; parse and validate those as data instead.

Choose the Name and the Location

Never reuse the client's filename on disk. Generate a UUID, append an extension derived from the detected type, and keep the original name in the database for display only (HTML-escaped when rendered).

javascript
const uploadRoot = "/srv/uploads"; // outside the application directory const target = path.join(uploadRoot, crypto.randomUUID() + ".png"); // Defensive check: the resolved path must stay under the root if (!path.resolve(target).startsWith(uploadRoot + path.sep)) throw new Error("path escape");

Better still, avoid local disk entirely. Object storage keeps uploads off the application server, cannot execute anything, and supports presigned URLs so the browser uploads directly without the file passing through your API.

Serve Files Safely

Serving an uploaded SVG from your application origin means any script inside it runs with your cookies. Serve downloads from a separate domain and set headers that stop the browser interpreting the content:

javascript
app.get("/files/:id", async (req, res) => { const file = await lookupFile(req.params.id); // validated id, ownership checked res.set({ "Content-Type": file.mime, // the detected type, never user input "Content-Disposition": `attachment; filename="${file.id}"`, "X-Content-Type-Options": "nosniff", "Content-Security-Policy": "default-src 'none'; sandbox" }); res.send(await readFromObjectStorage(file.id)); });

nosniff stops type guessing, attachment forces a download, and the sandboxed CSP neutralises any script if the file is displayed anyway.

Process Untrusted Files Defensively

Re-encode images so the stored file is one you generated, not one the user crafted:

javascript
const sharp = require("sharp"); const clean = await sharp(req.file.buffer).rotate().png().toBuffer(); // drops EXIF and hidden payloads

For documents, run an antivirus scan (ClamAV is the common self-hosted choice) and keep the file quarantined until the scan passes. Never automatically extract archives; a small zip can expand to gigabytes.

Quick Quiz
Question 1 of 2

Why is checking the file extension insufficient for validating an upload?

Key Takeaways

  • Uploads can lead to code execution, path traversal, stored XSS and denial of service.
  • Validate by content (magic bytes plus parsing or re-encoding), enforce size and count limits, allowlist MIME types.
  • Generate filenames on the server and store files outside the web root, preferably in object storage.
  • Serve files from a separate origin with nosniff, attachment and a sandboxed CSP.
  • Re-encode images, scan documents, never auto-extract archives.

Next lesson: The OWASP Top 10 Explained — the industry's standard list of the most critical web application risks.

File Upload Security - Cyber Security | CodeYourCraft | CodeYourCraft