Avatars, attachments, CSV imports. It is also a common way to get malware on your server if you are careless.
Form setup
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf" value="...">
<input type="file" name="avatar" accept="image/jpeg,image/png,image/webp">
<button type="submit">Upload</button>
</form>
Validate before you move
$file = $_FILES['avatar'] ?? null;
if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException('Upload failed');
}
$maxBytes = 2 * 1024 * 1024; // 2 MB
if ($file['size'] > $maxBytes) {
throw new RuntimeException('File too large');
}
$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('Invalid file type');
}
Check MIME type from file contents, not the client-supplied filename. evil.php.jpg is still a problem if you trust the extension alone.
Store outside public or serve safely
Best option: save uploads outside public/ and serve them through a PHP script that checks the user is allowed to view the file. If they must be public (static avatars), use a random filename and never allow .php execution in the upload directory.
$ext = $allowed[$mime];
$name = bin2hex(random_bytes(16)) . '.' . $ext;
$dest = dirname(__DIR__) . '/storage/uploads/' . $name;
if (!move_uploaded_file($file['tmp_name'], $dest)) {
throw new RuntimeException('Could not save file');
}
// Save $name in the database linked to the user
Web server hardening
- Disable PHP execution in the upload directory (Apache
php_flag engine off) - Set a disk quota or max upload size in php.ini
- Scan or reject double extensions
- Never use the original filename as the stored path
Uploads are trust boundaries. Treat every file as hostile until you have checked size, type, and destination.

