PHP cookbook · Intermediate

Handle file uploads securely in PHP

Accept user image uploads safely: validate type and size, avoid executable files, and store them outside the web root.

Approach

Never trust the client's filename or MIME type. Check the upload error code and size, detect the real type with finfo, map allowed types to extensions, generate a random filename, and move the file outside the public directory (or to object storage). Serve files through a controller or signed URLs with Content-Disposition and X-Content-Type-Options: nosniff.

Solution

<?php
declare(strict_types=1);

function storeAvatar(array $file, string $storageDir): string
{
    if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
        throw new RuntimeException('Upload failed');
    }
    if ($file['size'] > 2 * 1024 * 1024) {
        throw new RuntimeException('File too large (max 2 MB)');
    }
    $mime = (new finfo(FILEINFO_MIME_TYPE))->file($file['tmp_name']);
    $ext = match ($mime) {
        'image/jpeg' => 'jpg',
        'image/png' => 'png',
        'image/webp' => 'webp',
        default => throw new RuntimeException('Only JPEG, PNG or WebP images are allowed'),
    };
    // Re-encode to strip metadata and any embedded payloads
    $image = imagecreatefromstring(file_get_contents($file['tmp_name'])) ?: throw new RuntimeException('Invalid image');
    $name = bin2hex(random_bytes(16)) . '.' . $ext;
    $path = rtrim($storageDir, '/') . '/' . $name;
    match ($ext) {
        'jpg' => imagejpeg($image, $path, 85),
        'png' => imagepng($image, $path),
        'webp' => imagewebp($image, $path, 85),
    };
    return $name;
}

$name = storeAvatar($_FILES['avatar'] ?? [], '/var/app/storage/avatars');

Pitfalls

  • Storing uploads under public/ where a .php file could be executed.
  • Trusting $_FILES['x']['type'] (client-controlled).
  • Forgetting server limits: upload_max_filesize, post_max_size and Nginx client_max_body_size.

Variations

  • Upload directly to S3 with pre-signed POST URLs and validate after upload with a worker.
  • Scan non-image uploads with ClamAV before making them available.

Further reading: OWASP File Upload Cheat Sheet

Reference: https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html

More PHP recipes