PHP processes uploaded images, PDFs, and documents submitted from <form enctype="multipart/form-data"> through the $_FILES superglobal. Because attackers frequently disguise malicious PHP scripts as images, secure upload handlers never trust client filenames or browser-supplied MIME types.
Validate every upload through a strict sequence before moving it out of PHP's temporary directory.
flowchart TD
A["Receive $_FILES Entry"] --> B{"error === UPLOAD_ERR_OK?"}
B -->|"No"| ERR["Reject with Error Message"]
B -->|"Yes"| C{"Size <= 2 MB?"}
C -->|"No"| ERR
C -->|"Yes"| D["Inspect Real Bytes with finfo"]
D -->|"Allowed MIME?"| E["Generate Random Filename"]
D -->|"Disallowed"| ERR
E --> F["move_uploaded_file() Outside Public Root"]
Always inspect file bytes on the server using finfo (FILEINFO_MIME_TYPE) rather than reading $_FILES['file']['type'], which browsers send and attackers can forge easily.
<?php
declare(strict_types=1);
function storeUploadedAvatar(array $file, string $storageDir): string
{
if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
throw new RuntimeException('Upload failed or no file was selected.');
}
if (($file['size'] ?? 0) > 2 * 1024 * 1024) {
throw new RuntimeException('File exceeds the 2 MB size limit.');
}
$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('Only JPG, PNG, and WebP images are allowed.');
}
$safeName = bin2hex(random_bytes(16)) . '.' . $allowed[$mime];
$destination = rtrim($storageDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $safeName;
if (!move_uploaded_file($file['tmp_name'], $destination)) {
throw new RuntimeException('Could not save uploaded file.');
}
return $safeName;
}
Save uploaded files outside your public web root whenever possible, or disable PHP execution inside the uploads folder in Nginx/Apache. Generating a random filename via random_bytes(16) prevents directory traversal and filename collisions.
$_FILES['avatar']['name'] extension: A user can rename shell.php to avatar.jpg, or upload shell.php.jpg. Map the extension strictly from server-detected MIME types.rename() or copy() instead of move_uploaded_file(): move_uploaded_file() specifically verifies the source file was uploaded through PHP's HTTP POST mechanism.upload_max_filesize and post_max_size in php.ini: Large uploads fail silently with empty $_POST and $_FILES if post_max_size is too small.enctype="multipart/form-data" do on an HTML form?It tells the browser to encode binary file contents alongside text fields in the POST request. Without it, $_FILES remains empty.
$file['error'] === UPLOAD_ERR_OK first?PHP reports partial uploads, missing files, and php.ini size limit errors through numeric error codes in $file['error'] before your code even checks file bytes.
Store them outside public/ (or in object storage like S3) and stream them through a controlled download endpoint or CDN.
Sign in to track your learning journey, earn industry-recognized certificates, and join our elite developer community.
Quick Access With
Experiment with the code from this lesson in our interactive playground.