Skip to content

PHP File Upload — Secure File Handling, Validation, and Storage

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about PHP File Upload. We cover key concepts, practical examples, and best practices to help you master this topic.

PHP file upload allows users to send files from their browser to the server through HTML forms, where $_FILES captures the file metadata and move_uploaded_file stores it securely with proper validation.

Why It Matters

File upload is one of the most common web features and one of the most dangerous. A poorly implemented upload handler allows attackers to upload executable PHP files, malware, or large files that fill your disk. Understanding secure file upload -- validating type, size, and content -- is essential for any application that accepts user files.

Real-World Use

Social media platforms upload profile pictures and media content. Cloud storage services like Dropbox handle multi-gigabyte file uploads. Job sites accept PDF resumes. E-commerce sites upload product images. WordPress media library uploads images, PDFs, and videos. DodaTech's support portal allows users to upload diagnostic logs for analysis.

What You Will Learn

  • HTML form setup for file uploads
  • The $_FILES superglobal structure
  • File validation: type, size, extension, content
  • Secure file storage with random filenames
  • Multiple file upload handling
  • Error codes and troubleshooting
  • Upload progress tracking

Learning Path

flowchart LR
  A[Headers] --> B[File Upload
You are here] B --> C[JSON API] C --> D[Error Handling] D --> E[Classes and OOP] style B fill:#f90,color:#fff

HTML Form for File Uploads

File upload forms require specific attributes:

<form method="post" action="upload.php" enctype="multipart/form-data">
    <input type="hidden" name="MAX_FILE_SIZE" value="5242880">
    <input type="file" name="document" accept=".pdf,.jpg,.png">
    <button type="submit">Upload</button>
</form>

The enctype="multipart/form-data" is required. Without it, files are not sent. The optional MAX_FILE_SIZE hidden field hints the browser to limit file size, but you must still validate on the server.

The $_FILES Structure

When a file is uploaded, $_FILES contains an array with this structure:

$_FILES['document'] = [
    'name' => 'report.pdf',          // Original filename
    'type' => 'application/pdf',     // MIME type (from client)
    'tmp_name' => '/tmp/phpXXXXXX',  // Temporary path
    'error' => UPLOAD_ERR_OK,        // Error code
    'size' => 1024000,               // Size in bytes
];

Basic Upload Handler

A secure upload handler with validation:

<?php
$uploadDir = __DIR__ . '/uploads/';
$maxFileSize = 5 * 1024 * 1024;  // 5MB
$allowedExtensions = ['pdf', 'jpg', 'png', 'gif'];
$allowedMimeTypes = ['application/pdf', 'image/jpeg', 'image/png', 'image/gif'];

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    die('Invalid request method');
}

if (!isset($_FILES['document'])) {
    die('No file uploaded');
}

$file = $_FILES['document'];
$errors = [];

// Check upload error
if ($file['error'] !== UPLOAD_ERR_OK) {
    $errorMessages = [
        UPLOAD_ERR_INI_SIZE => 'File exceeds server size limit',
        UPLOAD_ERR_FORM_SIZE => 'File exceeds form size limit',
        UPLOAD_ERR_PARTIAL => 'File was only partially uploaded',
        UPLOAD_ERR_NO_FILE => 'No file was uploaded',
        UPLOAD_ERR_NO_TMP_DIR => 'Missing temporary folder',
        UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
    ];
    die($errorMessages[$file['error']] ?? 'Unknown upload error');
}

// Validate file size
if ($file['size'] > $maxFileSize) {
    die('File too large. Maximum is 5MB.');
}

// Validate extension
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($extension, $allowedExtensions)) {
    die('Invalid file extension');
}

// Validate MIME type (by file content, not client-reported)
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);

if (!in_array($mimeType, $allowedMimeTypes)) {
    die('Invalid file type');
}

// Generate a secure random filename
$newFilename = bin2hex(random_bytes(16)) . '.' . $extension;
$destination = $uploadDir . $newFilename;

// Ensure upload directory exists
if (!is_dir($uploadDir)) {
    mkdir($uploadDir, 0755, true);
}

// Move the file
if (move_uploaded_file($file['tmp_name'], $destination)) {
    echo "File uploaded successfully: $newFilename\n";
    echo "Original name: " . htmlspecialchars($file['name']) . "\n";
    echo "Size: " . number_format($file['size']) . " bytes\n";
    echo "Type: " . htmlspecialchars($mimeType) . "\n";
} else {
    echo "Failed to move uploaded file.";
}

Multiple File Uploads

Handle multiple files with array notation in the form:

<form method="post" enctype="multipart/form-data">
    <input type="file" name="files[]" multiple accept="image/*">
    <button type="submit">Upload Images</button>
</form>

PHP processing:

<?php
$uploadDir = __DIR__ . '/uploads/';
$uploaded = [];
$errors = [];

foreach ($_FILES['files']['name'] as $index => $name) {
    $file = [
        'name' => $_FILES['files']['name'][$index],
        'type' => $_FILES['files']['type'][$index],
        'tmp_name' => $_FILES['files']['tmp_name'][$index],
        'error' => $_FILES['files']['error'][$index],
        'size' => $_FILES['files']['size'][$index],
    ];

    if ($file['error'] !== UPLOAD_ERR_OK) {
        $errors[] = "File '$name' failed: error code {$file['error']}";
        continue;
    }

    $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
    $newName = bin2hex(random_bytes(8)) . '.' . $ext;
    $dest = $uploadDir . $newName;

    if (move_uploaded_file($file['tmp_name'], $dest)) {
        $uploaded[] = $newName;
    } else {
        $errors[] = "Failed to save '$name'";
    }
}

echo "Uploaded: " . implode(', ', $uploaded) . "\n";
if ($errors) {
    echo "Errors: " . implode(', ', $errors) . "\n";
}

File Upload Security Checklist

1. Validate File Extension

$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$allowed = ['jpg', 'png', 'gif', 'pdf'];
if (!in_array($ext, $allowed)) {
    die('Extension not allowed');
}

2. Validate MIME Type by Content

$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);

Do not trust $_FILES['file']['type'] -- it is sent by the client and can be faked.

3. Validate File Size

$maxSize = 10 * 1024 * 1024;  // 10MB
if ($file['size'] > $maxSize || $file['size'] === 0) {
    die('Invalid file size');
}

4. Use Random Filenames

$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$newName = bin2hex(random_bytes(16)) . '.' . $ext;

Never use user-supplied filenames. They can contain path traversal characters like ../.

5. Store Outside Web Root When Possible

Store uploads outside the document root and serve them through a PHP script that adds security checks.

6. Disable Execution in Upload Directory

Add an .htaccess file in your uploads directory:

<FilesMatch "\.(php|pl|py|jsp|asp|sh|cgi)$">
    Order Deny,Allow
    Deny from all
</FilesMatch>

Or for Nginx:

location /uploads/ {
    location ~ \.php$ { deny all; }
}

Upload Error Codes

Constant Value Meaning
UPLOAD_ERR_OK 0 Success
UPLOAD_ERR_INI_SIZE 1 Exceeds upload_max_filesize in php.ini
UPLOAD_ERR_FORM_SIZE 2 Exceeds MAX_FILE_SIZE in form
UPLOAD_ERR_PARTIAL 3 Only partially uploaded
UPLOAD_ERR_NO_FILE 4 No file was uploaded
UPLOAD_ERR_NO_TMP_DIR 6 Missing temporary folder
UPLOAD_ERR_CANT_WRITE 7 Failed to write to disk

PHP Configuration for Uploads

Set appropriate limits in php.ini:

file_uploads = On
upload_max_filesize = 10M
post_max_size = 12M
max_file_uploads = 20
upload_tmp_dir = /tmp

Common Mistakes

1. Not Checking upload_max_filesize

If a file exceeds upload_max_filesize, $_FILES is still set but error is UPLOAD_ERR_INI_SIZE. Always check the error code.

2. Trusting the Client-Reported MIME Type

$mime = $_FILES['file']['type'];  // Can be faked!

Use finfo to detect the actual file type from content.

3. Using User Filenames Without Sanitization

move_uploaded_file($tmp, 'uploads/' . $_FILES['file']['name']);
// Path traversal: name could be "../../etc/passwd"

Always generate a new random filename.

4. Not Limiting File Size

Large files can fill your disk and cause denial of service. Always enforce both upload_max_filesize and application-level limits.

5. Storing Uploads in the Web Root

Uploads in the web root are directly accessible. Users could upload HTML files with JavaScript and perform XSS. Store uploads outside web root or restrict access.

Practice Questions

  1. Why is enctype="multipart/form-data" required for file uploads? It tells the browser to encode the form data as multipart MIME, which is required for file binary data.

  2. How do you determine the actual file type of an upload? Use finfo_file() with FILEINFO_MIME_TYPE to examine the file content, not the client-reported type.

  3. Why should you use random filenames for uploaded files? To prevent path traversal attacks and filename collisions. Random names prevent users from guessing or accessing each other's files.

  4. What does move_uploaded_file do that rename does not? move_uploaded_file performs additional security checks to ensure the file was actually uploaded via PHP, preventing local file inclusion attacks.

  5. Challenge: Build a complete file gallery that allows image uploads, validates type and size, generates thumbnails, and displays uploaded images.

Mini Project: Image Upload with Validation

Build a secure image uploader:

<?php
define('UPLOAD_DIR', __DIR__ . '/gallery/');
define('MAX_SIZE', 5 * 1024 * 1024);
define('ALLOWED_TYPES', ['image/jpeg', 'image/png', 'image/webp']);
define('ALLOWED_EXTS', ['jpg', 'jpeg', 'png', 'webp']);

function uploadImage(array $file): array {
    if ($file['error'] !== UPLOAD_ERR_OK) {
        return ['success' => false, 'error' => 'Upload error code: ' . $file['error']];
    }

    if ($file['size'] > MAX_SIZE || $file['size'] === 0) {
        return ['success' => false, 'error' => 'Invalid file size'];
    }

    $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
    if (!in_array($ext, ALLOWED_EXTS)) {
        return ['success' => false, 'error' => 'Extension not allowed'];
    }

    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mime = finfo_file($finfo, $file['tmp_name']);
    finfo_close($finfo);
    if (!in_array($mime, ALLOWED_TYPES)) {
        return ['success' => false, 'error' => 'Invalid image type'];
    }

    if (!is_dir(UPLOAD_DIR)) {
        mkdir(UPLOAD_DIR, 0755, true);
    }

    $newName = bin2hex(random_bytes(12)) . '.' . $ext;
    $dest = UPLOAD_DIR . $newName;

    if (!move_uploaded_file($file['tmp_name'], $dest)) {
        return ['success' => false, 'error' => 'Failed to save file'];
    }

    return ['success' => true, 'filename' => $newName];
}

FAQ

What is the maximum file size I can upload?

Controlled by upload_max_filesize (default 2MB) and post_max_size in php.ini. Increase them if needed. Also check your web server's limits.

Why does my upload fail with a blank page?

Check the PHP error log. Common causes: file exceeds upload_max_filesize, post_max_size too small, memory_limit too low, or execution timeout.

How do I upload large files (100MB+)?

Configure upload_max_filesize and post_max_size, set memory_limit high enough, increase max_execution_time. Consider chunked uploads for very large files.

What is the difference between temporary and permanent storage?

Uploaded files are stored in a temporary location (tmp_name). They are deleted when the script ends. Use move_uploaded_file to make them permanent.

Can I upload files via AJAX?

Yes, use FormData API in JavaScript. The server-side handling is identical to form-based uploads.

What is Next

Now that you can handle file uploads, proceed to JSON API to build RESTful API endpoints that return structured data. Then read Error Handling to learn robust error management.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro