Incomplete Identification of Uploaded File Variables (PHP)
Description
Incomplete Identification of Uploaded File Variables occurs in PHP applications when file upload handling code doesn't properly validate all aspects of the uploaded file data. PHP's $_FILES superglobal contains multiple variables for each upload (name, type, tmp_name, error, size), and attackers can manipulate client-side values. Trusting the client-provided filename or MIME type without validation leads to various attacks.
Risk
Attackers can upload malicious files by manipulating the filename extension. Client-provided MIME types can be forged to bypass content filters. Path traversal in filenames can overwrite critical files. File extension mismatches enable code execution (uploading PHP as image). Double extensions and null bytes can bypass filters. The tmp_name could potentially be manipulated in certain configurations.
Solution
Never trust client-provided filename or MIME type. Generate server-side filenames. Validate file content using magic bytes/signatures. Use allowlists for permitted extensions. Check the actual file content type server-side. Sanitize filenames, removing path components. Store uploads outside web root. Use separate domains for user content.
Common Consequences
| Impact | Details |
|---|---|
| Integrity | Scope: File Overwrite Path traversal can overwrite system files. |
| Confidentiality | Scope: Code Execution Uploading PHP files leads to remote code execution. |
| Security | Scope: Content Injection Malicious files served to users. |
Example Code + Solution Code
Vulnerable Code
<?php
// VULNERABLE: Trusting client-provided filename
function uploadVulnerable1() {
if (isset($_FILES['upload'])) {
$filename = $_FILES['upload']['name']; // Client-controlled!
$dest = "/var/www/uploads/" . $filename;
// Attacker: filename = "../../config.php" (path traversal)
// Attacker: filename = "shell.php" (code execution)
move_uploaded_file($_FILES['upload']['tmp_name'], $dest);
}
}
// VULNERABLE: Trusting client-provided MIME type
function uploadVulnerable2() {
$allowed = ['image/jpeg', 'image/png', 'image/gif'];
$type = $_FILES['upload']['type']; // Client-controlled!
// Attacker sets Content-Type: image/jpeg but uploads PHP
if (in_array($type, $allowed)) {
move_uploaded_file(
$_FILES['upload']['tmp_name'],
"/uploads/" . $_FILES['upload']['name']
);
}
}
// VULNERABLE: Incomplete extension check
function uploadVulnerable3() {
$filename = $_FILES['upload']['name'];
$ext = pathinfo($filename, PATHINFO_EXTENSION);
// Bypassable: shell.php.jpg, shell.pHp, shell.php%00.jpg
if ($ext !== 'php') {
move_uploaded_file(
$_FILES['upload']['tmp_name'],
"/uploads/" . $filename
);
}
}
// VULNERABLE: Only checking size
function uploadVulnerable4() {
if ($_FILES['upload']['size'] > 0 &&
$_FILES['upload']['size'] < 1000000) {
// Size doesn't indicate file type!
move_uploaded_file(
$_FILES['upload']['tmp_name'],
"/uploads/" . $_FILES['upload']['name']
);
}
}
// VULNERABLE: Double extension bypass
function uploadVulnerable5() {
$filename = $_FILES['upload']['name'];
// Gets last extension only
if (preg_match('/\.(jpg|png|gif)$/', $filename)) {
// Bypassed: shell.php.jpg (Apache might execute as PHP)
move_uploaded_file(
$_FILES['upload']['tmp_name'],
"/uploads/" . $filename
);
}
}
// VULNERABLE: Case-sensitive check
function uploadVulnerable6() {
$filename = $_FILES['upload']['name'];
$ext = pathinfo($filename, PATHINFO_EXTENSION);
// Bypassed: shell.PHP, shell.PhP
if ($ext !== 'php' && $ext !== 'phtml') {
move_uploaded_file(
$_FILES['upload']['tmp_name'],
"/uploads/" . $filename
);
}
}
?>
Fixed Code
<?php
// SAFE: Complete file upload validation
class SafeFileUploader {
private const UPLOAD_DIR = '/var/www/uploads/';
private const MAX_SIZE = 5 * 1024 * 1024; // 5MB
// Allowed MIME types and their valid extensions
private const ALLOWED_TYPES = [
'image/jpeg' => ['jpg', 'jpeg'],
'image/png' => ['png'],
'image/gif' => ['gif'],
'application/pdf' => ['pdf']
];
public function upload(array $file): array {
// Check for upload errors
if ($file['error'] !== UPLOAD_ERR_OK) {
return ['success' => false, 'error' => $this->getErrorMessage($file['error'])];
}
// Check file size
if ($file['size'] > self::MAX_SIZE || $file['size'] === 0) {
return ['success' => false, 'error' => 'Invalid file size'];
}
// Verify it's an actual uploaded file
if (!is_uploaded_file($file['tmp_name'])) {
return ['success' => false, 'error' => 'Not a valid upload'];
}
// Detect MIME type from file content (not client-provided)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$detectedType = $finfo->file($file['tmp_name']);
// Verify detected type is allowed
if (!array_key_exists($detectedType, self::ALLOWED_TYPES)) {
return ['success' => false, 'error' => 'File type not allowed'];
}
// Get client-provided extension
$clientExt = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
// Verify extension matches detected type
if (!in_array($clientExt, self::ALLOWED_TYPES[$detectedType])) {
return ['success' => false, 'error' => 'Extension mismatch'];
}
// Additional content validation for images
if (strpos($detectedType, 'image/') === 0) {
if (!$this->validateImage($file['tmp_name'])) {
return ['success' => false, 'error' => 'Invalid image file'];
}
}
// Generate safe filename (never use client filename)
$safeFilename = $this->generateSafeFilename($clientExt);
$destination = self::UPLOAD_DIR . $safeFilename;
// Move file
if (!move_uploaded_file($file['tmp_name'], $destination)) {
return ['success' => false, 'error' => 'Upload failed'];
}
return [
'success' => true,
'filename' => $safeFilename,
'path' => $destination
];
}
private function generateSafeFilename(string $extension): string {
// Generate random filename, no client input
return bin2hex(random_bytes(16)) . '.' . $extension;
}
private function validateImage(string $path): bool {
// Verify image can be processed
$info = getimagesize($path);
if ($info === false) {
return false;
}
// Check for PHP code in image
$content = file_get_contents($path);
if (preg_match('/<\?php|<\?=/i', $content)) {
return false;
}
return true;
}
private function getErrorMessage(int $error): string {
$messages = [
UPLOAD_ERR_INI_SIZE => 'File exceeds upload_max_filesize',
UPLOAD_ERR_FORM_SIZE => 'File exceeds MAX_FILE_SIZE',
UPLOAD_ERR_PARTIAL => 'File only partially uploaded',
UPLOAD_ERR_NO_FILE => 'No file uploaded',
UPLOAD_ERR_NO_TMP_DIR => 'Missing temp directory',
UPLOAD_ERR_CANT_WRITE => 'Failed to write to disk',
UPLOAD_ERR_EXTENSION => 'Upload blocked by extension'
];
return $messages[$error] ?? 'Unknown error';
}
}
// Usage
$uploader = new SafeFileUploader();
$result = $uploader->upload($_FILES['upload']);
if ($result['success']) {
echo "File uploaded: " . $result['filename'];
} else {
echo "Error: " . $result['error'];
}
?>
<?php
// SAFE: Alternative approach with ImageMagick reprocessing
class SafeImageUploader {
public function uploadImage(array $file): ?string {
// Basic validation first
if ($file['error'] !== UPLOAD_ERR_OK) {
return null;
}
// Detect actual type
$finfo = new finfo(FILEINFO_MIME_TYPE);
$type = $finfo->file($file['tmp_name']);
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($type, $allowedTypes)) {
return null;
}
// Reprocess image to strip any embedded code
$image = new Imagick($file['tmp_name']);
$image->stripImage(); // Remove metadata
// Generate safe filename
$filename = bin2hex(random_bytes(16)) . '.png';
$path = '/var/www/uploads/' . $filename;
// Save as PNG (safe format)
$image->setImageFormat('png');
$image->writeImage($path);
$image->destroy();
return $filename;
}
}
// SAFE: Storing uploads outside web root
class SecureUploader {
private const STORAGE_DIR = '/var/storage/uploads/'; // Outside web root
public function upload(array $file): ?string {
// Validation...
$filename = bin2hex(random_bytes(16));
$path = self::STORAGE_DIR . $filename;
move_uploaded_file($file['tmp_name'], $path);
// Store mapping in database
$this->storeMapping($filename, $file['name'], $file['type']);
return $filename;
}
// Serve files through PHP (not direct access)
public function serve(string $id): void {
$file = $this->getFileInfo($id);
if (!$this->userCanAccess($file)) {
http_response_code(403);
exit;
}
$path = self::STORAGE_DIR . $file['storage_name'];
header('Content-Type: ' . $this->getSafeContentType($path));
header('Content-Disposition: attachment; filename="' .
$this->sanitizeFilename($file['original_name']) . '"');
header('X-Content-Type-Options: nosniff');
readfile($path);
}
}
?>
Exploited in the Wild
Web Shell Uploads
PHP files uploaded as images led to complete server compromise.
Config File Overwrites
Path traversal in filenames overwrote configuration files.
Content Injection
Malicious SVG/HTML files served to users enabled XSS.
Tools to test/exploit
-
Burp Suite — modify upload requests.
-
fuxploider — file upload scanner.
-
Custom scripts for extension/MIME manipulation.
CVE Examples
-
CVEs from PHP file upload vulnerabilities.
-
Path traversal via filename in upload handlers.
References
-
MITRE. "CWE-616: Incomplete Identification of Uploaded File Variables (PHP)." https://cwe.mitre.org/data/definitions/616.html
-
PHP Documentation. File Upload Handling.