My image upload checks $_FILES['file']['type'] == 'image/jpeg' and saves to /uploads with the original filename. A security scan said it allows remote code execution. How should I handle uploads safely?
DEV3 Developer Forum
How do I validate and store uploaded files securely in PHP?
Every post is reviewed first. Usually reviewed within 24 hours.
1 reply
$_FILES['file']['type'] and the original name are supplied by the client, so they can be anything. An attacker uploads shell.php with a fake image/jpeg type and requests it.
Safe handling
function saveImage(array $f): string
{
if ($f['error'] !== UPLOAD_ERR_OK) throw new RuntimeException('Upload failed');
if (!is_uploaded_file($f['tmp_name'])) throw new RuntimeException('Bad upload');
if ($f['size'] > 5 * 1024 * 1024) throw new RuntimeException('Too large');
// detect the real type from content
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($f['tmp_name']);
$map = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp'];
if (!isset($map[$mime])) throw new RuntimeException('Type not allowed');
// ensure it really is an image
if (@getimagesize($f['tmp_name']) === false) throw new RuntimeException('Not an image');
// generate our own name, never use the client's
$name = bin2hex(random_bytes(16)) . '.' . $map[$mime];
$dest = '/var/www/storage/uploads/' . $name;
if (!move_uploaded_file($f['tmp_name'], $dest)) throw new RuntimeException('Cannot save');
return $name;
}
Rules that matter
- Choose the extension from the detected MIME type via an allow list, not from the user filename.
- Store uploads outside the web root and serve through a script (
readfilewith properContent-TypeandContent-Disposition), or at least disable PHP execution in the upload directory. - Apache
.htaccessin the upload folder (when it must be public):
<FilesMatch "\.(php|phtml|phar|php\d)$">
Require all denied
</FilesMatch>
Nginx: do not pass the upload location to PHP-FPM.
4. Re-encode images with GD or Imagick (imagecreatefromjpeg then imagejpeg) to strip embedded payloads and metadata.
5. Limit size in both php.ini and in code; limit counts and rate.
6. SVG is XML and can contain scripts: sanitize it or do not accept it.
7. Check the PDF/ZIP type with finfo as well, and scan with ClamAV if users share files.
Pitfalls
- A double extension
image.php.jpgcan run on misconfigured Apache withAddHandler. Renaming files prevents that. mime_content_typeandfinfoneed thefileinfoextension.- Do not trust
getimagesizealone; polyglot files pass. That is why you also keep files non-executable.
The MIME type lookup tool helps when building the allow list.