Check the upload error code, enforce a size limit, detect the real MIME type with finfo instead of trusting the browser, then save with move_uploaded_file under a name you generate, in a directory the web server will not execute. Each step closes a different gap.
What the PHP manual advises
The PHP manual page on handling file uploads includes an example that begins with a warning to check the $_FILES structure and values thoroughly. It guards against an undefined or array valued error field, which it labels a corruption attack. It then checks the error code, checks the size against a limit, and says not to trust the MIME value in $_FILES, because that comes from the client. It checks the type itself with finfo.
The manual for move_uploaded_file adds that the function only moves files that were uploaded through PHP, which protects against being tricked into moving other files. It also warns that if the destination exists, it will be overwritten.
A safe sequence
if (!isset($_FILES['upfile']['error']) || is_array($_FILES['upfile']['error'])) {
throw new RuntimeException('Invalid parameters.');
}
if ($_FILES['upfile']['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException('Upload failed.');
}
if ($_FILES['upfile']['size'] > 1000000) {
throw new RuntimeException('File too large.');
}
$finfo = new finfo(FILEINFO_MIME_TYPE);
$type = $finfo->file($_FILES['upfile']['tmp_name']);
$allowed = ['jpg' => 'image/jpeg', 'png' => 'image/png'];
$ext = array_search($type, $allowed, true);
if ($ext === false) {
throw new RuntimeException('Invalid file format.');
}
$name = bin2hex(random_bytes(16)) . '.' . $ext;
move_uploaded_file($_FILES['upfile']['tmp_name'], '/srv/uploads/' . $name);
The extension comes from the detected type, never from the submitted file name.
What each step prevents
| Step | Gap it closes |
|---|---|
| Check the structure | Malformed or array valued fields |
| Check the error code | Partial or failed uploads treated as success |
| Check the size | Disk and memory abuse |
| Detect type with finfo | Client supplied type spoofing |
| Generate the name | Path tricks, overwrites, odd characters |
| Store outside the web root | Uploaded script being run as code |
Where to store files
Put uploads outside the document root, or in a directory where the web server will not run scripts. If files must be reachable, serve them through a script that sets the content type and disposition yourself. An uploaded PHP file in a web served folder is a classic route to a web shell, which is why the name and location matter more than the check.
Steps summary
- Validate the shape of
$_FILESand the error code. - Enforce size in code and in server configuration.
- Detect the type with
finfo, and map it to an extension from an allow list. - Create the file name yourself.
- Save outside the web root.
- Serve through a controlled path, with a download disposition for non images.
- Keep image libraries updated if you resize uploads.
Limits of content checks
A file can have a valid image header and still carry other data. Re-encoding images through a library can strip extra content, but then the library itself is part of your attack surface. Patch it.
Server limits come first
PHP and the web server both have their own upload size limits, and a request over the limit may be rejected before your code runs. Set them to match your application limit, so users get a clear error and your code is not asked to handle huge bodies. Also make sure the temporary upload directory is not shared with other applications.
Quick check
Can I trust $_FILES type?
No. The PHP manual example says not to trust it, since the client sets it.
Why not keep the original file name?
It can contain path characters or an executable extension, and it can collide with an existing file.
Safeguard's SBOM and dependency scanning can list the libraries your upload code depends on. A free scan is at gold.safeguard.sh/scan.
Sources
Last reviewed: 19 October 2026.