File Extension Bypass
debt(d5/e5/b3/t7)
Closest to 'specialist tool catches it' (d5). The detection_hints list Semgrep as the tool, with a specific code pattern targeting pathinfo() or strrchr() without MIME validation. This is not caught by the compiler or a default linter — it requires a configured SAST rule (Semgrep) to flag the pattern, placing it squarely at d5.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix requires replacing extension-based validation with finfo_file(), renaming files to UUID filenames, moving the storage location outside the webroot, and auditing web server configuration for executable extensions. This is more than a one-liner — it touches the upload handler, storage logic, and server config — but is contained within the file-upload subsystem rather than being codebase-wide, placing it at e5.
Closest to 'localised tax' (b3). The applies_to scope is web context file upload handling. The burden is real but isolated: only the upload and file-serving components carry the weight of proper MIME validation, UUID naming, and webroot segregation. The rest of the codebase is largely unaffected, keeping this at b3.
Closest to 'serious trap' (t7). The misconception field explicitly states that developers believe a blocklist of dangerous extensions is sufficient, when in reality servers can execute .php5, .phar, and even .jpg files as PHP under certain configurations. The common_mistakes reinforce multiple independently plausible wrong assumptions (last-extension-only checks, null byte blindness, no magic-byte verification). This contradicts intuitive reasoning about how file type checking should work and is a well-documented but frequently missed gotcha, scoring t7.
Also Known As
TL;DR
Explanation
Upload filters that check only file extension are easily bypassed: shell.php.jpg may execute as PHP on misconfigured servers; %00 null bytes truncated extension checks in older PHP; shell.pHp evades case-sensitive checks; .phtml, .php5, and .phar are overlooked PHP-executable extensions. Correct validation: allowlist safe MIME types verified server-side with finfo_file(), store uploads outside the document root, serve through a PHP controller that sets Content-Type explicitly, and never rely on extension alone. Rename uploaded files server-side to strip any attacker-controlled extension.
How It's Exploited
Common Misconception
Why It Matters
Common Mistakes
- Checking only the last extension — file.php.jpg passes a .jpg check.
- Not stripping null bytes from filenames — .php%00.jpg becomes .php on old PHP.
- Allowlisting by extension without verifying actual file content via magic bytes.
- Web server configured to execute .php5, .phtml, .phar as PHP — all must be blocked.
Code Examples
// Extension check bypassable:
$ext = strtolower(pathinfo($_FILES['f']['name'], PATHINFO_EXTENSION));
if ($ext === 'jpg') move_uploaded_file(...); // file.php.jpg passes!
// Safe approach:
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($_FILES['f']['tmp_name']);
$allowed = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($mime, $allowed, true)) die('Blocked');
// Validate content type from file content, not extension:
function validateUpload(string $tmpPath): string {
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($tmpPath);
$allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($mimeType, $allowed, true)) {
throw new InvalidFileTypeException("Disallowed type: {$mimeType}");
}
// Store with a safe server-generated name:
$ext = ['image/jpeg'=>'jpg','image/png'=>'png','image/gif'=>'gif','image/webp'=>'webp'][$mimeType];
$safeName = bin2hex(random_bytes(16)) . '.' . $ext;
return $safeName;
}