Arbitrary File Upload
debt(d5/e5/b5/t7)
Closest to 'specialist tool catches it' (d5). The term's detection_hints list semgrep as the tool, which is a specialist SAST scanner that can detect the pattern of move_uploaded_file without MIME/extension validation or storing inside webroot. A default linter won't catch this — it requires a security-focused static analysis rule, placing it squarely at d5.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix involves multiple coordinated changes: adding finfo validation, adding extension allowlist checks, moving storage outside webroot, and serving files through a controller instead of direct URLs. While each individual change is small, they span the upload handler, storage configuration, and serving layer — a significant refactor within the upload subsystem, potentially touching multiple files.
Closest to 'persistent productivity tax' (b5). Secure file upload handling imposes an ongoing tax: every new upload feature must follow the same pattern (validate magic bytes, store outside webroot, serve via controller, sanitize filenames). It applies only to web contexts per applies_to, but within that context it affects any feature involving file uploads. It's not as pervasive as b7 (it doesn't shape every change), but it's a persistent concern that slows down multiple work streams involving user-generated content.
Closest to 'serious trap — contradicts how a similar concept works elsewhere' (t7). The misconception field is explicit: developers believe checking the file extension is sufficient validation. This is a serious trap because the 'obvious' approach (check extension, maybe check Content-Type header) feels complete and secure but is thoroughly bypassable via double extensions (.php.jpg), null bytes, and polyglot files. The common_mistakes reinforce this — trusting client-supplied MIME types and Content-Type headers seems reasonable since those fields appear authoritative, but they are entirely attacker-controlled. Developers from other ecosystems where file uploads don't lead to RCE (e.g., static file servers) will especially guess wrong.
Also Known As
TL;DR
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
# 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
Common Misconception
Why It Matters
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
// No validation — attacker uploads a PHP webshell
move_uploaded_file($_FILES['f']['tmp_name'], 'uploads/'.$_FILES['f']['name']);
$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);