How do I handle a file upload safely in PHP?
Read the file from $_FILES, verify its real MIME type with finfo, generate your own filename, and move it with move_uploaded_file() into a directory that does not execute PHP.
The form needs the right encoding, which is the most common thing to forget:
<form method="post" enctype="multipart/form-data">
<input type="file" name="avatar">
<button>Upload</button>
</form>Without enctype="multipart/form-data", $_FILES is empty and nothing works.
#The handler
$file = $_FILES['avatar'] ?? null;
if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException('Upload failed.');
}
if ($file['size'] > 2 * 1024 * 1024) {
throw new RuntimeException('Maximum size is 2 MB.');
}
// Check the real type, not the reported one
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp'];
if (!isset($allowed[$mime])) {
throw new RuntimeException('Images only.');
}
// Generate the name yourself — never reuse the client's
$name = bin2hex(random_bytes(16)) . '.' . $allowed[$mime];
$dest = __DIR__ . '/../uploads/' . $name;
if (!move_uploaded_file($file['tmp_name'], $dest)) {
throw new RuntimeException('Could not save the file.');
}#The three rules
Never trust $_FILES['x']['type']. It is supplied by the browser and trivially forged. finfo inspects the actual bytes.
Never use the client's filename. ../../index.php is a path traversal; shell.php.jpg may still execute on a misconfigured server. Generate a random name and derive the extension from the verified type.
Store uploads outside the web root, or disable PHP execution in that directory:
# uploads/.htaccess
php_flag engine offAn uploaded file that the server will execute is a remote shell.
#Size limits live in three places
upload_max_filesize and post_max_size in php.ini, plus your own check. A file larger than post_max_size arrives with an empty $_FILES and no obvious error, which is why the ?? null guard above matters.