Blog · E.19 · Technical

File upload vulnerabilities explained

“Upload your profile picture” sounds harmless. But an upload lets an untrusted stranger place a file of their choosing onto your server - and if that file is a script and it lands somewhere executable, that's a web shell and full server compromise from a photo field. Here's how uploads become RCE, the bypasses that defeat naive checks, the other damage (stored XSS, path traversal), and how to build an upload that's actually safe.

File UploadWeb ShellRCEStored XSSWeb AppSec
Uploads: Web Shell → RCE · Extension / Content-Type / Magic-byte Bypass · Stored XSS (SVG/HTML) · Path Traversal · Fix = Non-executable Storage + Layered Validation Uploads: Web Shell → RCE · Extension / Content-Type / Magic-byte Bypass · Stored XSS (SVG/HTML) · Path Traversal · Fix = Non-executable Storage + Layered Validation
// TL;DR

A file upload lets an untrusted user place attacker-chosen content on your server. The worst case: upload a server-side script (web shell) to an executable locationremote code execution and full compromise. Attackers bypass naive checks via extension / double-extension, Content-Type spoofing, magic-byte polyglots, null bytes - so any single check can be defeated. Even without RCE: stored XSS (SVG/HTML), path traversal, DoS, malware. The single most important fix: store uploads where they're never executed (outside web root / object storage). Then layer: content-based allow-list, size limits, random filenames, safe serving (nosniff, forced download), image re-encoding, malware scan. Detail below - part of every web app test.

// 01 Why uploads are inherently risky

Every other input to your app is data your code processes. A file upload is different: it lets an untrusted user place a file of their choosing - potentially a program - onto your infrastructure. That's a categorically higher-risk operation, because the danger isn't only what your app does with the file's contents, but what happens if the file itself is later executed, served, or interpreted. The feature looks benign - avatars, document uploads, import files - which is exactly why it's under-tested. But because the upload accepts attacker-controlled content, it has to be validated on multiple dimensions and, above all, stored somewhere it can do no harm. Miss that and a photo field becomes an execution primitive.

// 02 How an upload becomes remote code execution

The worst-case chain is short and devastating. An attacker uploads a server-side script - a .php, .jsp or .aspx file containing a web shell. If the app stores it in a directory the web server will execute scripts from, the attacker simply requests that file's URL, the server runs it, and now they have a shell - the ability to run arbitrary commands, read data, and pivot deeper. That's full server compromise from a single upload, often pre-authentication if the upload is public. It's the same critical endpoint as insecure deserialization or SSTI, reached by a simpler road. The decisive factor is where the file lands: an upload directory that executes code is the vulnerability; one that never executes code contains the damage.

// 03 The bypasses that defeat naive checks

01

Extension tricks

Double extensions (shell.php.jpg), alternate extensions the server still executes, case tricks.

02

Content-Type spoofing

Send a malicious file with an allowed Content-Type header - trivially forged by the client.

03

Magic-byte polyglots

A file with valid image magic bytes and executable code - passes a “is it an image?” check.

04

Filename injection

Null bytes and path characters to confuse the parser or traverse directories.

The lesson is blunt: any single validation - extension, Content-Type, or magic bytes alone - can be bypassed. A tester's job is to try each; a defender's job is to assume each will be defeated and rely on layered validation plus non-executable storage rather than one clever check.

// 04 Beyond RCE: XSS, traversal & more

Even where code execution isn't reachable, insecure uploads cause serious damage. Stored XSS: an uploaded SVG or HTML file can contain script; if the app serves it inline from your domain, viewing it runs the attacker's script in your users' browsers - a persistent client-side compromise. Path traversal: a malicious filename (../../) can cause the file to be written outside the intended directory, potentially overwriting critical files. Denial of service: huge files or decompression bombs exhaust storage or memory. Malware distribution: your app becomes a host for malicious files sent to others. And uploaded documents that are XML under the hood (DOCX, SVG) can carry XXE. The point: an upload has many failure modes, not just one.

// 05 Building a secure upload

The defence is layered, with one dominant control. Above all: store uploads where they can never be executed - outside the web root, or in object storage - so even a bypassed check can't become RCE. Then layer: validate type by content against an allow-list (not extension or Content-Type alone); enforce a maximum size; generate a new random filename rather than trusting the user's (killing traversal and filename tricks); serve files safely with a correct Content-Type, Content-Disposition: attachment where appropriate, and X-Content-Type-Options: nosniff to stop browsers reinterpreting them; re-encode or process images to strip embedded payloads; and scan for malware where relevant. No single control suffices - but the combination, anchored by non-executable storage, makes uploads safe. Every web app engagement we run tests the upload paths across all these vectors, per our methodology.

// 06 Frequently asked questions

Why are file uploads dangerous?

An upload lets an untrusted user place a file of their choosing on your server. Without proper validation and handling, an attacker can upload a malicious script (web shell) that, if it lands somewhere executable, gives remote code execution and full compromise. Even without RCE, insecure uploads enable stored XSS (SVG/HTML), path traversal, DoS and malware distribution.

How do attackers bypass upload restrictions?

Extension and double-extension tricks; Content-Type spoofing; magic-byte polyglots (a valid image that's also code); null bytes and filename injection; and case or alternate extensions the server still executes. Any single check - extension, Content-Type or magic bytes alone - can be bypassed, so validation must be layered and, critically, the upload location must not be executable.

Can a file upload lead to RCE?

Yes - the worst case. If an attacker uploads a server-side script (PHP, JSP, ASP) to a directory the web server executes from, requesting that file runs their code, giving a web shell and typically full control. The most important defence is storing uploads where they're never executed - outside the web root or in object storage - so even a bypassed check can't become RCE.

How do you build a secure upload?

Layer defences: validate type by content against an allow-list (not extension/Content-Type alone); enforce a max size; generate a new random filename and store outside the web root or in object storage so files can't execute; serve with a safe Content-Type, forced download where appropriate, and nosniff; scan for malware; and re-encode images to strip payloads. No single control suffices, but the combination - especially non-executable storage - makes uploads safe.

// 07 Related reading

UG

Usama Gul

Founder & Penetration Testing Lead, CyberFortify

Turns “upload your avatar” into a proven web shell in testing — then hands teams the one control that matters most: storage that never executes what a stranger uploaded.

Got an upload feature?

Avatars, documents, imports - any upload can become a web shell. We test every bypass and prove the impact safely, then hand you the layered fix anchored on non-executable storage.

Scope a web app test → Web app testing →