Choose your stack

Node.js · Express

Fix unrestricted file upload in Node.js and Express

Decide the type from the bytes, and never let the client pick the name.

Open the fix card
Python · Flask

Fix unrestricted file upload in Python and Flask

Decide the type from the bytes, and never let the client pick the name.

Open the fix card
Java · Spring Boot

Fix unrestricted file upload in Java and Spring Boot

Decide the type from the bytes, and never let the client pick the name.

Open the fix card
C# · ASP.NET Core

Fix unrestricted file upload in C# and ASP.NET Core

Decide the type from the bytes, and never let the client pick the name.

Open the fix card
TypeScript · Next.js

Fix unrestricted file upload in Next.js

Decide the type from the bytes, not from file.type or the name.

Open the fix card
Python · FastAPI

Fix unrestricted file upload in Python and FastAPI

Decide the type from the bytes and never let the client pick the name.

Open the fix card
Python · Django

Fix unrestricted file upload in Python and Django

Decide the type from the bytes and never let the client pick the name.

Open the fix card
Go · net/http

Fix unrestricted file upload in Go and net/http

Decide the type from the bytes, and never serve uploads from the web root.

Open the fix card
Ruby · Rails

Fix unrestricted file upload in Ruby on Rails

Decide the type from the bytes, and never serve uploads from the web root.

Open the fix card
PHP · Laravel

Fix unrestricted file upload in PHP and Laravel

Validate with max:512, mimes:png,jpg,jpeg (the type guessed from the contents) and extensions:png,jpg,jpeg (the client's name), require getClientMimeType() to equal getMimeType(), store() on a private disk so hashName() picks the name, and serve with Storage::download() plus X-Content-Type-Options: nosniff.

Open the fix card

How it happens

  • The upload is written into the folder the site serves as static files, under the client's file name. Static file handlers in all four stacks choose the Content-Type from the extension and send the file inline, so profile.html comes back as text/html and avatar.svg as image/svg+xml, and opening either URL runs the uploaded script with the site's origin, cookies and APIs. On a server that runs scripts by extension, such as PHP, the same mistake runs the attacker's code on the server.
  • The client chooses every part of the upload: the file name, the Content-Type and the bytes. Browsers derive the Content-Type from the extension, and any script can send whatever it likes, so checking file.mimetype, getContentType() or IFormFile.ContentType checks a string the attacker wrote. An HTML page named avatar.png and declared image/png passes it.
  • The file name is used as a path. Werkzeug, Spring on Tomcat and ASP.NET Core passed ../escaped.png and an absolute path through unchanged in these cards (multer's parser, busboy, keeps only the base name), and joining the name onto the upload folder wrote outside it. A name another user already uploaded replaces their file.
  • Name checks fail in predictable ways: a blocklist misses .svg, .xhtml and whatever handler the server gains next; a test for .png anywhere in the name accepts avatar.png.html; and a random name that keeps the client's extension still lets the extension decide how the file is served. A signature check alone fails too: a PNG with an HTML page appended is a valid image and, served as text/html, a working page.
  • Without a limit the framework reads whatever arrives. multer's fileSize and Flask's MAX_CONTENT_LENGTH are unlimited unless set, while Spring Boot caps each file at 1MB and Kestrel caps request bodies at 30,000,000 bytes (about 28.6 MB): limits for the whole application, not for the feature.

Fix principles

  • Decide what the feature needs and allow only that: here PNG and JPEG avatars up to 512 KiB. Require the extension, the declared Content-Type and the file signature to name the same type (89 50 4E 47 0D 0A 1A 0A for PNG, FF D8 FF for JPEG). The signature is the check a client cannot relabel; the other two catch disguised and mistaken uploads, and the extension rule refuses avatar.png.html outright.
  • Enforce the size limit while the request is read, not after it is stored: multer's limits.fileSize, Flask's request.max_content_length plus an exact check on the file, ASP.NET Core's WithFormOptions(multipartBodyLengthLimit), and in Spring the app-wide spring.servlet.multipart limits plus a per-feature check in code.
  • Never use the client's file name as a path or an identity. Store the file under a name the server generates (128 random bits plus an extension taken from the verified type), create it with create-new semantics so nothing is overwritten, and keep the original name only as display text if you need it.
  • Store uploads outside the web root and serve them through a handler that accepts only names it generated, sets the verified Content-Type, X-Content-Type-Options: nosniff and Content-Disposition: attachment. An <img> element still shows an attachment; only a direct visit downloads it. Serving user files from a separate origin (another registrable domain) keeps a missed check from running script with your cookies.
  • Treat the signature check as a floor, not proof of a harmless file. Re-encoding images with a maintained library, run under resource limits, drops appended data and metadata; scanning for known malware is a separate layer. Neither replaces the allowlist or the serving headers.
  • Keep access control in view: an upload endpoint behind cookie authentication needs CSRF protection like any other form post, and private files need an authorization check on the serving route, because an unguessable name is not access control.

The scenario

Users upload a profile picture. POST /avatars takes multipart/form-data with one file part named avatar. The feature accepts PNG and JPEG images of at most 512 KiB (524,288 bytes) and answers 201 with the URL the avatar is served from, 413 when the file is too large, 415 when its extension, declared Content-Type and first bytes are not all PNG or all JPEG, and 400 when no file was sent. The app also serves a web root (public/, with an index.html) as static files, the way many apps serve their front end. The vulnerable and wrong implementations store uploads in public/uploads/, inside it; the fixed one stores them in storage/, outside it, and serves them from GET /avatars/{name}. Each test starts from a fresh temporary folder built from seed.json, which is byte-identical in the four stacks.

How the cards prove it

  • Run each card's tests in three modes: the fixed implementation must pass, and the vulnerable and the plausible-but-wrong implementations must fail. run-all.mjs does this for every card.
  • The regression tests send what an attacker would: an HTML page and an SVG with script, an HTML page named avatar.png and declared image/png, a PNG with an HTML page appended named avatar.png.html, the same file named avatar.png (accepted, but it must come back only as an image download with nosniff), ../escaped.png and an absolute file name (no file may appear outside the upload folders), a file one byte over 512 KiB, and two users uploading avatar.png. Functional tests keep PNG and JPEG avatars, an upper-case .JPG and a file of exactly 512 KiB working.
  • The tests assert on status codes, files and headers, not on a browser, so a headless Chrome 154 probe against the Node card checked the browser side on 2026-10-10. Opened from the vulnerable app, profile.html, avatar.svg and avatar.png.html all ran their script (the last is the polyglot, served as text/html). The polyglot stored as a .png and served inline as image/png displayed as an image without running anything, which is why that test checks the defensive headers rather than an executed script. The fixed route's response was downloaded instead of displayed, an <img> element still showed it, and the SVG ran no script when shown through <img>.
  • Static analysis is weak here. Semgrep 1.180.0 with the p/default, p/security-audit and p/owasp-top-ten packs (2026-10-10) reported nothing about uploads in any stack, and the semgrep-rules repository has no rule for multer, request.files, MultipartFile or IFormFile. CodeQL (read, not executed) models Flask's FileStorage.save() as a path sink but has no sink for Spring's MultipartFile.transferTo(), and its only CWE-434 queries are js/http-to-file-access, which would flag the fixed Node code's write, and cs/web/file-upload, which only knows ASP.NET Web Forms. .NET's CA3003 flags file names from IFormFile only in MVC actions. Nothing checked notices a missing type check or the serving headers; each card's scannerRules records what was run and what it found.

Identifiers

CWE-434 (unrestricted upload of file with dangerous type) is the weakness the card is named after: the vulnerable code accepts HTML and SVG that the browser then runs. CWE-646 (reliance on file name or extension of externally-supplied file) describes the wrong implementation, which trusts the declared Content-Type and keeps the client's extension; CWE-73 (external control of file name or path) describes the vulnerable code's use of the client's file name as the storage path. All three map to OWASP Top 10:2025 A06 Insecure Design. CWE-770 covers the missing size limit, which MITRE notes is another common reading of "unrestricted file upload"; it is not mapped to any Top 10:2025 category. The 2025 CWE Top 25 ranks CWE-434 12th and CWE-770 25th. The consequences fall under other categories: an uploaded page served from the site is stored XSS (CWE-79, A05:2025) and ../ in a file name is path traversal (CWE-22, A01:2025). CAPEC-1 is the only attack pattern MITRE links to CWE-434; CAPEC-209 (XSS using MIME type mismatch) is linked to CWE-646, CAPEC-635 describes deceptive file name extensions such as avatar.png.html, and CAPEC-650 is the web shell the same mistake becomes on a server that runs uploaded scripts. The ASVS 5.0.0 entries are V5 File Handling (5.2.1 size, 5.2.2 extension and content, 5.3.1 no execution, 5.3.2 generated paths, 5.4.1 Content-Disposition), 3.2.1 (attachment disposition or sandboxing for user files) and 3.4.4 (nosniff).

Attack patterns:

References

  1. OWASP File Upload Cheat Sheet
  2. OWASP ASVS 5.0.0, V5 File Handling
  3. OWASP ASVS 5.0.0, V3 Web Frontend Security (3.2 Unintended Content Interpretation, 3.4 Browser Security Mechanism Headers)
  4. OWASP Top 10:2025 A06 Insecure Design
  5. CWE-434: Unrestricted Upload of File with Dangerous Type
  6. CWE-646: Reliance on File Name or Extension of Externally-Supplied File
  7. CWE-73: External Control of File Name or Path
  8. CWE-770: Allocation of Resources Without Limits or Throttling
  9. CAPEC-209: XSS Using MIME Type Mismatch
  10. CAPEC-635: Alternative Execution Due to Deceptive Filenames
  11. WHATWG MIME Sniffing: matching an image type pattern
  12. MDN: X-Content-Type-Options
  13. MDN: SVG as an image (scripts are disabled only in image contexts)
  14. RFC 6266: Use of the Content-Disposition Header Field in HTTP

Practice in the browser

Guides on this weakness

Guide

Design a safer file-upload pipeline

Treat upload receipt, content processing, storage, and later delivery as separate security decisions.

Guide

Bound API work, not just request counts

Protect availability and operating cost with limits on input, per-request work, concurrency, queues, and downstream spending.

Guide

Extract archives without escaping the destination

Prevent traversal, link tricks, overwrites and decompression exhaustion while preserving legitimate file extraction.

Guide

Secure WebRTC signaling, TURN relays and media servers

WebRTC encrypts media, but signaling decides whose keys you trust and TURN relays wherever it is allowed. Authorize every signaling message, mint short-lived TURN credentials, deny internal addresses and harden media servers.

Guide

Apply security limits to GraphQL, WebSocket and gRPC operations

Carry authorization and resource budgets past the initial HTTP request into resolvers, messages and long-lived streams.

Guide

Bound background jobs and make retries safe

Keep queues, workers and paid dependencies within a defined budget when clients retry, cancel or disappear.

Guide

Bound agent spending, memory and recovery

Enforce budgets and stop conditions outside the model, and make retained context and side effects recoverable.

Guide

Contain cascading failures and rogue agents

Stop a looping, compromised or drifting agent from spreading harm: kill switches, budget and rate limits, circuit breakers, behavior monitoring, blast-radius limits, quarantine, rollback and incident response, all enforced outside the agent.

Guide

Parse bounded JSON and XML into an explicit application schema

Separate parser capabilities from schema rules, reject ambiguous and coercible inputs, and implement the boundary in a complete Python project.

Guide

Keep HTTP services up under application-layer DDoS

Bound every phase of an HTTP request, patch for the HTTP/2 attack classes, rate limit at the edge and the origin, and shed load deliberately, with nginx, Envoy and Node.js settings tested against slow clients and bursts.

Reference

Abuse case library: tests and controls by feature

Recurring abuse cases for sign-up and login, password reset, MFA enrolment, invites, file upload, payments and coupons, webhooks, search, export, admin and AI chat or agents: the attacker goal, preconditions, a concrete test and the control, each linked to CWE and CAPEC.

Reference

Threat pattern library for threat modeling

Thirty-two recurring threat patterns grouped by identity, authorization, parsers, concurrency, integrations, data exposure, supply chain, availability and AI agents, each with where it shows up on a data-flow diagram, mitigations, a verification step and STRIDE, CWE and CAPEC links, plus a YAML catalog and a script that lists patterns per diagram element.

Guide

Prevent file inclusion: LFI, RFI, dynamic imports and template names

Stop request data from choosing which file, module or template your application loads: local and remote file inclusion in PHP, dynamic require() and import() in Node.js, importlib and template names in Python, Thymeleaf view names in Spring, render in Rails, and the allowlist fix that works in all of them.

Guide

Consume third-party APIs safely (OWASP API10:2023)

Treat every upstream and third-party API response as untrusted input: validate it against a schema, set connect and read timeouts, cap the response size, refuse to follow redirects blindly, keep TLS verification on, and scope the credentials you hold for each provider.

Recipe

Call a hosted LLM API safely

A code-first checklist for wiring up an OpenAI or Anthropic API call from a backend: keep keys server-side, redact PII before prompts and logs, choose provider retention settings deliberately, validate structured output and still treat it as untrusted, and set timeouts, retries and per-user cost caps.

Guide

Sandbox model-generated code and computer-use agents

Run the code an agent writes, and agents that drive a browser or desktop, inside a boundary built for hostile code: a disposable runtime with no secrets and no default network, hard resource limits, and a person in front of every side effect.