File Upload Vulnerabilities
Also known as: Unrestricted file upload, Dangerous file type upload, Insecure file upload
A flaw in which an application accepts files without verifying what they are or controlling where they land, so an uploaded file can be stored in an executable location or served back unsafely; it is prevented by validating real content, storing files outside the web root and serving them as inert downloads.
How it works
A file upload vulnerability exists when an application accepts a file from a user without properly checking what the file is, controlling where it is stored or limiting how it is later served. The filename, the declared content type and the extension are all supplied by the client, so none of them can be trusted as proof of what the bytes actually are.
The danger depends on what happens after the file lands. If it is stored inside the web root under its original name and the server is configured to run files of that type, an upload can become code that runs on the server. Even when it cannot run, a file served with the wrong content type can execute in a visitor's browser, a path component in the filename can overwrite other files, and oversized uploads can exhaust disk or memory.
The fix is layered and structural. Decide an allow-list of permitted types and verify the real content, for example by checking file signatures rather than the extension. Store files outside the web root or in object storage with no execute permission, give every file a random server-generated name, enforce size limits, and serve downloads with a fixed content type and an attachment disposition.
Detection is a backstop. Watch for uploads with executable or double extensions, requests that reach uploaded paths directly, and unexpected processes started by the web server account. Antivirus or content scanning on the upload path catches known-bad files but never replaces the controls above.
Walk through it
- 1Read the upload handler
- 2Name the missing validation
- 3Choose where files must live
- Names, limits and safe serving
- Fix the handler and verify
A pre-release review flags the ticket-attachment feature. Users attach screenshots to support tickets. Read the handler as a reviewer and note what it trusts from the client and where it puts the file.
1// POST /tickets/:id/attachments (multipart form, field: file)2router.post("/tickets/:id/attachments", requireSession, upload.single("file"), async (req, res) => {3 const file = req.file;4 const target = path.join(WEB_ROOT, "public", "uploads", file.originalname);5 await fs.rename(file.path, target);6 res.json({ url: "/uploads/" + file.originalname });7});Spot it
- Uploads whose filename has an executable or script extension, or a double extension such as a document name followed by a script suffix.
- A request to an uploaded file's path shortly after the upload, especially from the same source and for a type the application never serves as a page.
- Unexpected child processes, shells or outbound connections started by the web server account.
- New files with executable extensions appearing under a served uploads directory, seen by file integrity monitoring.
- Upload size or request-rate outliers from one source against an upload endpoint.
Web access log
ts=2026-10-11T10:02:11Z method=POST path=/tickets/812/attachments status=200 src=203.0.113.77 upload_name="report.pdf.php"
ts=2026-10-11T10:02:19Z method=GET path=/uploads/report.pdf.php status=200 src=203.0.113.77Host audit log
ts=2026-10-11T10:02:19Z event=process_start parent=webserver user=www-data note=unexpected_for_this_account
ts=2026-10-11T10:02:41Z event=file_created path=/var/www/public/uploads/ type=executable note=fim_alertsplSplunk: uploads with script or double extensions
index=web sourcetype=access method=POST path="*/attachments"
| regex upload_name="(?i)\.(php|jsp|aspx?|sh|exe)(\.|$)"
| stats count by src, upload_nameAllow-list expected extensions instead where you can; tune to the application's real upload types.
kqlKQL: web server account starting unexpected processes
DeviceProcessEvents
| where InitiatingProcessAccountName in ("www-data","iis apppool\\portal")
| summarize starts=count() by DeviceName, FileName
| where starts > 0Compare with a baseline of normal child processes before alerting.
Stop it
Validate the real content against an allow-list, never the extension or declared type
Accept only the file types the feature needs, and confirm them by inspecting the content, for example file signatures. Treat the client's filename and content type as untrusted labels.
Store files outside the web root or in object storage with no execute permission
Keep uploads where the web server cannot serve or run them directly, and hand them back through a controlled download route that sets a fixed content type and an attachment disposition.
Generate random server-side names, enforce size limits and scan the content
Never use the client's filename for storage, so path tricks and overwrites are impossible. Cap file size and count, reject dangerous extensions, and scan with antivirus as a backstop.
Validate content, store privately under a random name
Vulnerable
const target = path.join(WEB_ROOT, "public", "uploads", file.originalname);
await fs.rename(file.path, target);Hardened
const kind = await detectFileType(file.path); // inspects file signature
if (!kind || !ALLOWED_TYPES.has(kind.mime)) return res.status(415).end();
const storedName = crypto.randomUUID() + "." + kind.ext;
await fs.rename(file.path, path.join(PRIVATE_UPLOAD_DIR, storedName));PRIVATE_UPLOAD_DIR is outside the web root; the original filename is kept only as metadata.
Serve downloads as inert files
Hardened
Content-Type: <type from the allow-list>
Content-Disposition: attachment; filename="download"
X-Content-Type-Options: nosniff- Allow-list permitted file types per feature and verify content signatures server-side.
- Store uploads outside the web root or in object storage; disable script execution on any upload location.
- Use random server-generated names and keep the original filename only as display metadata.
- Enforce maximum size, count and rate per user at the proxy and in the application.
- Serve downloads with a fixed content type, Content-Disposition attachment and nosniff.
- Scan uploads with antivirus and quarantine failures; run the upload service with least privilege.
If it already happened
Disable the upload feature or block the route, remove execute permission from the uploads directory, and block the offending sources.
Find and remove every file uploaded since the flaw shipped that does not match an allowed type, review what the web server account ran, and rotate any secrets it could read.
Ship the fixed handler with content validation and private storage, restore from a known-good state if the host is in doubt, and re-enable the feature behind tests.
Add a review checklist item and static-analysis rule for client-controlled paths, and keep the upload and process detections above permanently.
Check yourself
1. Why is checking the file extension alone not enough to validate an upload?
2. Which storage choice most directly prevents an uploaded file from being run by the web server?
3. Why should the server generate a random name for each stored upload?