Insecure file upload
Avatars, invoices, résumés, support screenshots: almost every application lets users upload files. It looks innocent, but you are inviting a stranger to place a file of their choosing on your server. Handled badly, that file can be a script that the server executes, a page that attacks other visitors, or a bomb that swallows your disk. This lesson shows what can go wrong and how to design uploads safely.
Lab only
Uploading a harmful file to a real server is an attack on that system. In this course, upload tests are conducted only in your lab (for instance DVWA’s “File Upload” module or the matching Web Security Academy labs), and we deliberately provide no malicious file contents. Our exercises use harmless test files.
What is uploaded, really?
When a user uploads a file, the request carries several pieces of information, and each one is controlled by the user:
- The file name, including its extension and any special characters.
- The declared content type (for example “image/png”), written in the request by the browser, but modifiable with a proxy.
- The content itself: it may contain anything, whatever the extension claims.
- The size and, with multiple files, the number.
The golden rule you already know applies: nothing coming from the client can be trusted. A file called “holiday.png” may not be an image at all.
The main risks
| Risk | What can happen |
|---|---|
| Executable content on the server | If the uploaded file lands in a place where the server runs it as code, an attacker obtains code execution on the server. This is the most severe outcome. |
| Stored XSS | Files that browsers display as pages, such as HTML, SVG images or similar, can carry scripts that run in the site’s origin when opened. |
| Path manipulation | A crafted file name might place the file in an unexpected directory or overwrite an existing file. |
| Denial of service | Huge files, many files, or specially compressed archives that expand enormously can exhaust disk, memory or processor. |
| Malware distribution | Your site becomes a host for harmful files that other users download. |
| Data exposure | Uploaded files are accessible to people who should not see them (see IDOR, chapter 5). |
| Parser attacks | Vulnerabilities in the libraries that process the file: image converters, document readers, archive extractors. |
| Server-side requests | Some formats can cause a processing library to fetch external resources, leading back to SSRF. |
Weak defences and why they fail
As in previous lessons, the typical flawed approaches are easy to recognise:
- Checking the extension only on the client side. A script in the page can be bypassed in a second with a proxy.
- Trusting the declared content type. It is just a header the client wrote.
- A deny-list of dangerous extensions. There are many alternative extensions, case variants, double extensions and server-specific tricks. Deny-lists miss some.
- Checking only the first bytes of the file (a “magic number”). Useful, but a file can start like an image and contain something else afterward.
- Storing files in the web root with the original name and extension, and serving them directly.
The DVWA file upload module at different security levels illustrates several of these: the low level has no checks, the medium level trusts the declared content type, and the high level checks the extension and size but still has gaps. The impossible level shows a robust approach.
How testers examine an upload feature
In your lab, with harmless files, ask these questions and record the answers.
- What is accepted? Upload a normal image, a text file and a file of an unexpected type, all harmless. Which are accepted or rejected, and with what message?
- Where is the file stored and how is it served? Find the URL of the uploaded file. Does the URL contain your original name? Is it served from the same domain as the application?
- How is the type decided? In Repeater, change the declared content type while keeping a harmless file, and see whether the decision changes.
- What happens with an unusual name? Try a very long name, a name with spaces or special characters, and a name with no extension. Note how the application sanitises or fails.
- Are there size limits? A reasonable limit should exist. Do not try huge files on a real server; in the lab, test a size slightly above the displayed limit.
- Who can access the file? Can another account open your upload URL? Can an unauthenticated user?
- Does the application transform the file? Resizing images or re-encoding them is a good sign, because it discards extra content.
The PortSwigger Academy has a progression of upload labs that explain each weakness with a safe, prepared environment. Follow its instructions for the details of what to upload.
Designing a safe upload
The defender’s best practice combines several layers.
- Allowlist file types: accept only the formats the feature needs (for an avatar: a few image formats), and validate the extension against that list.
- Verify the actual content, not the headers: use a library to check that the file really is the declared type, and, for images, re-encode them with an image library, which strips anything that is not image data.
- Rename the file: generate a random name on the server and choose the extension yourself, based on your validated type. Never use the user’s name in the storage path.
- Store files outside the web root, or in a dedicated storage service, and serve them through the application, which can enforce access control and set safe headers.
- Prevent execution: configure the storage location so that nothing there is executed as code, whatever its name.
- Serve from a separate domain, or at least send safe headers (
Content-Dispositionto force a download where appropriate,X-Content-Type-Options: nosniff, and a restrictive CSP), so that uploaded files cannot run scripts in your site’s origin. - Limit size and quantity, and guard against archive bombs if you unpack archives.
- Scan for malware for features where users share files with each other.
- Require authentication and enforce access control on upload and on download.
- Process files in an isolated environment when using complex parsers, and keep those libraries updated (previous lesson).
Safe upload flow (pseudocode)
1. Require login; check size against the limit
2. Check the extension against an allowlist (for example a few image types)
3. Check the real content type with a library; reject if it does not match
4. Re-encode the image; discard the original bytes
5. Generate a random file name; store it outside the web root
6. Serve it through the application with nosniff and a restrictive policy
Tip
If a feature does not strictly need user-chosen file types, narrow it: “upload a profile photo” can accept just two or three image formats. The fewer formats, the less complexity and fewer parsers to attack.
Your turn: evaluate an upload feature
Use the file upload module in your lab (or the first Academy upload lab). With harmless files only, complete the seven questions above in your notebook, and conclude with a list of the weaknesses you observed and a safe-upload recommendation.
See a solution
For DVWA at the low level, expect notes like these:
Accepted: any file type, no restriction (low level)
Storage: web-accessible folder under the application, original name kept, URL displayed after upload
Type decision: none
Access: anyone with the URL can open the file
Weaknesses: no allowlist, original name used, stored in an executable location, no size limit observed
Recommendation: allowlist of image types, content verification and re-encoding, random file name,
storage outside the web root, no execution in the upload folder, size limit
At the medium and high levels, redo the tests and describe what changed and what a determined tester could still question. Reading the impossible level’s source shows how the design above looks in practice.
Key takeaways
- File name, declared type and content of an upload are all controlled by the user and cannot be trusted.
- Risks range from server-side code execution and stored XSS to denial of service, data exposure and parser attacks.
- Client-side checks, content-type headers, deny-lists and magic-number checks alone are weak.
- Design safely: allowlist, content verification and re-encoding, random names, storage outside the web root, no execution, safe headers, limits and access control.
- Test only with harmless files and only in your own lab or authorised environments.
We end this chapter with a more advanced but highly impactful weakness, which sits at the border between data and code: unsafe deserialization.