Arbitrary File Upload
Also Known As
unrestricted file upload
webshell upload
TL;DR
Accepting file uploads without validating type, extension, and content can allow PHP shell uploads and RCE.
Explanation
Unrestricted file upload lets attackers upload executable scripts (e.g. a PHP web shell) disguised with a harmless extension or MIME type. If stored in a web-accessible directory, the shell can be executed directly, giving full server access. Mitigation requires validating the actual file content with mime_content_type(), enforcing an extension allowlist, storing uploads outside the web root, and using random server-generated filenames.
How It's Exploited
# Upload shell.php with Content-Type: image/jpeg
# If server checks only Content-Type header (not finfo), shell is accepted
# GET /uploads/shell.php?cmd=id → RCE
# If server checks only Content-Type header (not finfo), shell is accepted
# GET /uploads/shell.php?cmd=id → RCE
Diagram
flowchart TD
UPLOAD[File uploaded] --> EXT{Extension check}
EXT -->|php phtml phar| REJECT1[Reject - executable]
EXT -->|allowed extension| MAGIC{Magic bytes check<br/>finfo FILEINFO_MIME_TYPE}
MAGIC -->|mismatch - jpg but PHP| REJECT2[Reject - disguised]
MAGIC -->|match - real image| STORE[Store safely]
STORE --> RENAME[Random filename<br/>no original name]
RENAME --> OUTSIDE[Outside webroot<br/>or CDN bucket]
OUTSIDE --> SERVE[Serve via PHP<br/>with Content-Type header<br/>X-Content-Type-Options: nosniff]
style REJECT1 fill:#f85149,color:#fff
style REJECT2 fill:#f85149,color:#fff
style STORE fill:#238636,color:#fff
style OUTSIDE fill:#238636,color:#fff
Watch Out
⚠ A PHP webshell uploaded as "shell.php.jpg" with a double extension can execute if the server is misconfigured to process any file containing ".php" in the name — extension blacklisting alone is insufficient.
Common Misconception
✗ Checking the file extension is sufficient validation. Attackers bypass extension checks with double extensions (.php.jpg), null bytes, and polyglot files that are simultaneously valid images and valid PHP.
Why It Matters
An attacker who uploads a PHP file to a web-accessible path has remote code execution — effectively full server compromise.
Common Mistakes
- Trusting the MIME type or Content-Type header sent by the client — both are attacker-controlled.
- Checking only the file extension but not actual file content (magic bytes).
- Storing uploaded files inside the webroot where the server will execute them.
- Using the original filename from the upload without sanitisation, enabling path traversal.
Avoid When
- Never allow uploads to a directory with PHP execution enabled — even a validated image can be re-requested as a PHP file if stored incorrectly.
- Do not use $_FILES["type"] for validation — it is supplied by the client and trivially spoofed.
- Avoid serving uploaded files through the same domain as the application without a Content-Disposition: attachment header.
When To Use
- Validate file type by reading magic bytes (finfo_file) rather than trusting the client-supplied MIME type or extension.
- Store uploaded files outside the web root or in object storage — never in a directory the web server can execute.
- Rename uploaded files to a random slug on save — never use the original filename in the stored path.
Code Examples
💡 Note
The bad example moves the file with the original user-supplied name to a web-accessible uploads directory — an attacker uploads shell.php and executes arbitrary code. The fix validates magic bytes, renames the file, and stores it outside the web root.
✗ Vulnerable
// No validation — attacker uploads a PHP webshell
move_uploaded_file($_FILES['f']['tmp_name'], 'uploads/'.$_FILES['f']['name']);
✓ Fixed
\$upload = \$_FILES['file'];
// 1. Verify actual MIME type via finfo (not browser-supplied Content-Type)
\$finfo = new \finfo(FILEINFO_MIME_TYPE);
\$mime = \$finfo->file(\$upload['tmp_name']);
\$allowed = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
if (!in_array(\$mime, \$allowed, true)) abort(415);
// 2. Random filename — never trust original name
\$ext = ['image/jpeg'=>'jpg','image/png'=>'png','image/webp'=>'webp','image/gif'=>'gif'][\$mime];
\$filename = bin2hex(random_bytes(16)) . '.' . \$ext;
// 3. Store outside webroot — never in a PHP-executable directory
move_uploaded_file(\$upload['tmp_name'], '/var/uploads/' . \$filename);
// 4. Serve via a controller that sets correct headers
header('Content-Type: ' . \$mime);
readfile('/var/uploads/' . \$filename);
References
Tags
🤝 Adopt this term
£79/year · your link shown here
Added
15 Mar 2026
Edited
31 Mar 2026
Views
33
🤖 AI Guestbook educational data only
|
|
Last 30 days
Agents 0
No pings yet today
No pings yesterday
Perplexity 10
Amazonbot 8
Unknown AI 3
Google 2
Ahrefs 2
ChatGPT 1
Also referenced
How they use it
crawler 25
pre-tracking 1
Related categories
⚡
DEV INTEL
Tools & Severity
🔴 Critical
⚙ Fix effort: Medium
⚡ Quick Fix
Validate MIME type server-side with finfo, check extension against allowlist, store outside webroot, serve via controller not direct URL
📦 Applies To
PHP 5.0+
web
🔍 Detection Hints
move_uploaded_file without MIME/extension validation or storing inside webroot
Auto-detectable:
✓ Yes
semgrep
⚠ Related Problems
🤖 AI Agent
Confidence: High
False Positives: Medium
✗ Manual fix
Fix: Medium
Context: Function
Tests: Update
CWE-434
CWE-646