Why Secure File Uploads Matter


Insecure file uploads are one of the top attack vectors for web applications. Without proper validation, attackers can upload malicious scripts, overwrite existing files, or exploit your server. This tutorial focuses on security-first file handling:
  • Server-side MIME type verification (never trust client-side checks alone)
  • File extension whitelist to block dangerous file types
  • File size limits to prevent denial-of-service attacks
  • Randomized file names to prevent path traversal and overwrites
  • Image dimension validation for profile pictures and thumbnails
  • Secure upload directory configuration with no script execution
  • Single and multiple file upload handling with progress feedback
By the end of this tutorial, you will have a reusable, production-ready file upload class that you can drop into any PHP project.

PHP File Upload with Validation: Secure Image & Document Upload Tutorial

Table Of Content

1 Prerequisites

  • PHP 8.0+ installed
  • Apache or PHP built-in server
  • Basic PHP and HTML knowledge
  • GD or Imagick extension (for image validation — usually pre-installed)
  • fileinfo extension enabled (for MIME type detection — enabled by default in PHP 8+)

2 How PHP File Uploads Work

When a user submits a form with enctype="multipart/form-data", PHP stores the uploaded file in a temporary directory and populates the $_FILES superglobal array with file metadata:

$_FILES["fieldname"] contains:

name — The original file name from the user's computer
type — The MIME type reported by the browser (DO NOT trust this alone!)
tmp_name — Path to the temporary file on the server
error — Error code (0 = success, 1-8 = various errors)
size — File size in bytes

The upload process:

1. User selects a file and submits the form.
2. PHP receives the file and stores it in a temp directory (sys_get_temp_dir()).
3. Your code validates the file (type, size, extension, content).
4. If valid, move it from temp to your upload directory using move_uploaded_file().
5. If invalid, reject it and show an error message.

Critical rule: Never trust the file extension or browser-reported MIME type alone. Always verify the actual file content using PHP's finfo functions.

3 Project Setup

3.1 Create Project Structure

Create the project folder and subdirectories:

mkdir php-file-upload
cd php-file-upload
mkdir classes
mkdir uploads
mkdir uploads/images
mkdir uploads/documents

Make sure the uploads directory is writable by the web server:

chmod -R 755 uploads/

3.2 PHP Configuration (php.ini)

Ensure these settings are configured in your php.ini file:

file_uploads = On
upload_max_filesize = 10M
post_max_size = 12M
max_file_uploads = 10
memory_limit = 128M
max_execution_time = 60

Important notes:

post_max_size should always be slightly larger than upload_max_filesize to account for form overhead.
max_file_uploads controls how many files can be uploaded in a single request.

After changing php.ini, restart your web server (Apache/Nginx) for changes to take effect.

4 Secure File Upload Class

Create classes/FileUploader.php — this reusable class handles all upload logic with built-in security:

<?php

class FileUploader
{
    private string $uploadDir;
    private array $allowedMimeTypes;
    private array $allowedExtensions;
    private int $maxFileSize;
    private int $maxImageWidth;
    private int $maxImageHeight;
    private array $errors = [];
    private array $uploadedFiles = [];

    // Predefined allowed types
    public const IMAGE_TYPES = [
        "mime"       => ["image/jpeg", "image/png", "image/gif", "image/webp"],
        "extensions" => ["jpg", "jpeg", "png", "gif", "webp"]
    ];

    public const DOCUMENT_TYPES = [
        "mime"       => ["application/pdf", "application/msword", 
                         "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
                         "application/vnd.ms-excel",
                         "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
                         "text/plain", "text/csv"],
        "extensions" => ["pdf", "doc", "docx", "xls", "xlsx", "txt", "csv"]
    ];

    public function __construct(array $config = [])
    {
        $this->uploadDir         = rtrim($config["upload_dir"] ?? "uploads/", "/") . "/";
        $this->allowedMimeTypes  = $config["allowed_mime"] ?? self::IMAGE_TYPES["mime"];
        $this->allowedExtensions = $config["allowed_ext"] ?? self::IMAGE_TYPES["extensions"];
        $this->maxFileSize       = $config["max_size"] ?? 5 * 1024 * 1024; // 5MB default
        $this->maxImageWidth     = $config["max_width"] ?? 4096;
        $this->maxImageHeight    = $config["max_height"] ?? 4096;

        // Create upload directory if it doesn't exist
        if (!is_dir($this->uploadDir)) {
            mkdir($this->uploadDir, 0755, true);
        }
    }

    /**
     * Upload a single file
     */
    public function upload(array $file): ?array
    {
        $this->errors = [];

        // Check for upload errors
        if ($file["error"] !== UPLOAD_ERR_OK) {
            $this->errors[] = $this->getUploadErrorMessage($file["error"]);
            return null;
        }

        // Validate file
        if (!$this->validate($file)) {
            return null;
        }

        // Generate secure file name
        $extension   = $this->getSecureExtension($file["name"]);
        $newFileName = $this->generateSecureName($extension);
        $destination = $this->uploadDir . $newFileName;

        // Move the file
        if (!move_uploaded_file($file["tmp_name"], $destination)) {
            $this->errors[] = "Failed to move uploaded file";
            return null;
        }

        $result = [
            "original_name" => $file["name"],
            "saved_name"    => $newFileName,
            "path"          => $destination,
            "size"          => $file["size"],
            "size_readable" => $this->formatFileSize($file["size"]),
            "mime_type"     => $this->getActualMimeType($destination),
            "extension"     => $extension
        ];

        // Add image dimensions if it's an image
        if ($this->isImage($destination)) {
            $imageInfo = getimagesize($destination);
            $result["width"]  = $imageInfo[0];
            $result["height"] = $imageInfo[1];
        }

        return $result;
    }

    /**
     * Upload multiple files
     */
    public function uploadMultiple(array $files): array
    {
        $this->uploadedFiles = [];
        $this->errors = [];

        // Reorganize $_FILES array for multiple uploads
        $fileCount = count($files["name"]);

        for ($i = 0; $i < $fileCount; $i++) {
            $file = [
                "name"     => $files["name"][$i],
                "type"     => $files["type"][$i],
                "tmp_name" => $files["tmp_name"][$i],
                "error"    => $files["error"][$i],
                "size"     => $files["size"][$i]
            ];

            $result = $this->upload($file);

            if ($result) {
                $this->uploadedFiles[] = $result;
            }
        }

        return $this->uploadedFiles;
    }

    /**
     * Validate the uploaded file
     */
    private function validate(array $file): bool
    {
        $valid = true;

        // 1. Check file size
        if ($file["size"] > $this->maxFileSize) {
            $this->errors[] = "File \"{$file["name"]}\" exceeds the maximum size of " . $this->formatFileSize($this->maxFileSize);
            $valid = false;
        }

        // 2. Check file extension (whitelist)
        $extension = $this->getSecureExtension($file["name"]);
        if (!in_array($extension, $this->allowedExtensions, true)) {
            $this->errors[] = "File extension \".{$extension}\" is not allowed. Allowed: " . implode(", ", $this->allowedExtensions);
            $valid = false;
        }

        // 3. Check actual MIME type using finfo (NOT browser-reported type)
        $actualMime = $this->getActualMimeType($file["tmp_name"]);
        if (!in_array($actualMime, $this->allowedMimeTypes, true)) {
            $this->errors[] = "File type \"{$actualMime}\" is not allowed for \"{$file["name"]}\"";
            $valid = false;
        }

        // 4. For images — validate dimensions and that it's a real image
        if ($valid && $this->isImageMime($actualMime)) {
            $imageInfo = @getimagesize($file["tmp_name"]);
            if ($imageInfo === false) {
                $this->errors[] = "File \"{$file["name"]}\" is not a valid image";
                $valid = false;
            } else {
                if ($imageInfo[0] > $this->maxImageWidth || $imageInfo[1] > $this->maxImageHeight) {
                    $this->errors[] = "Image dimensions exceed maximum {$this->maxImageWidth}x{$this->maxImageHeight}px";
                    $valid = false;
                }
            }
        }

        // 5. Check for PHP code in file content (prevent disguised scripts)
        $content = file_get_contents($file["tmp_name"], false, null, 0, 1024);
        if (preg_match('/(<\?php|<\?=|errors[] = "File \"{$file["name"]}\" contains suspicious content";
            $valid = false;
        }

        return $valid;
    }

    /**
     * Get the actual MIME type using finfo (server-side detection)
     */
    private function getActualMimeType(string $filePath): string
    {
        $finfo = new finfo(FILEINFO_MIME_TYPE);
        return $finfo->file($filePath);
    }

    /**
     * Generate a secure random file name
     */
    private function generateSecureName(string $extension): string
    {
        return bin2hex(random_bytes(16)) . "_" . time() . "." . $extension;
    }

    /**
     * Extract and sanitize file extension
     */
    private function getSecureExtension(string $filename): string
    {
        $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
        // Remove any null bytes or special characters
        return preg_replace('/[^a-z0-9]/', '', $extension);
    }

    /**
     * Check if MIME type is an image type
     */
    private function isImageMime(string $mime): bool
    {
        return str_starts_with($mime, "image/");
    }

    /**
     * Check if a file is an image
     */
    private function isImage(string $filePath): bool
    {
        return $this->isImageMime($this->getActualMimeType($filePath));
    }

    /**
     * Format file size to human readable string
     */
    private function formatFileSize(int $bytes): string
    {
        $units = ["B", "KB", "MB", "GB"];
        $i = 0;
        while ($bytes >= 1024 && $i < count($units) - 1) {
            $bytes /= 1024;
            $i++;
        }
        return round($bytes, 2) . " " . $units[$i];
    }

    /**
     * Get human-readable upload error message
     */
    private function getUploadErrorMessage(int $errorCode): string
    {
        return match ($errorCode) {
            UPLOAD_ERR_INI_SIZE   => "File exceeds the server upload size limit",
            UPLOAD_ERR_FORM_SIZE  => "File exceeds the form upload size limit",
            UPLOAD_ERR_PARTIAL    => "File was only partially uploaded",
            UPLOAD_ERR_NO_FILE    => "No file was uploaded",
            UPLOAD_ERR_NO_TMP_DIR => "Server missing temporary folder",
            UPLOAD_ERR_CANT_WRITE => "Failed to write file to disk",
            UPLOAD_ERR_EXTENSION  => "A PHP extension blocked the upload",
            default               => "Unknown upload error"
        };
    }

    /**
     * Get validation errors
     */
    public function getErrors(): array
    {
        return $this->errors;
    }

    /**
     * Check if there are any errors
     */
    public function hasErrors(): bool
    {
        return !empty($this->errors);
    }
}

?>

Security features built into this class:

  • finfo-based MIME detection — Checks actual file content, not the browser-reported type
  • Extension whitelist — Only explicitly allowed extensions are accepted
  • Random file names — Uses cryptographic random bytes to prevent path traversal and filename guessing
  • Content scanning — Checks first 1KB for embedded PHP code in disguised files
  • Image validation — Uses getimagesize() to verify the file is a genuine image, not a renamed script
  • Proper error codes — Maps all PHP upload error codes to readable messages

5 Single File Upload (Image)

Create upload_image.php — handles single image upload with the FileUploader class:

<?php

require_once __DIR__ . "/classes/FileUploader.php";

$message = "";
$uploadResult = null;

if ($_SERVER["REQUEST_METHOD"] === "POST") {

    if (!isset($_FILES["image"]) || $_FILES["image"]["error"] === UPLOAD_ERR_NO_FILE) {
        $message = "Please select an image to upload.";
    } else {
        $uploader = new FileUploader([
            "upload_dir"   => "uploads/images",
            "allowed_mime" => FileUploader::IMAGE_TYPES["mime"],
            "allowed_ext"  => FileUploader::IMAGE_TYPES["extensions"],
            "max_size"     => 5 * 1024 * 1024,   // 5MB
            "max_width"    => 2000,
            "max_height"   => 2000
        ]);

        $uploadResult = $uploader->upload($_FILES["image"]);

        if ($uploader->hasErrors()) {
            $message = implode("
", $uploader->getErrors()); } else { $message = "Image uploaded successfully!"; } } } ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Single Image Upload</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"> </head> <body class="bg-light"> <div class="container mt-5"> <div class="row justify-content-center"> <div class="col-md-6"> <div class="card shadow-sm"> <div class="card-header bg-primary text-white"> <h4 class="mb-0">Upload Image</h4> </div> <div class="card-body"> <?php if (!empty($message)): ?> <div class="alert alert-<?= $uploadResult ? 'success' : 'danger' ?>"> <?= $message ?> </div> <?php endif; ?> <?php if ($uploadResult): ?> <div class="mb-3 text-center"> <img src="<?= htmlspecialchars($uploadResult["path"]) ?>" class="img-fluid rounded" style="max-height: 300px;"> </div> <table class="table table-sm"> <tr><td><b>Original Name:</b></td><td><?= htmlspecialchars($uploadResult["original_name"]) ?></td></tr> <tr><td><b>Saved As:</b></td><td><?= $uploadResult["saved_name"] ?></td></tr> <tr><td><b>Size:</b></td><td><?= $uploadResult["size_readable"] ?></td></tr> <tr><td><b>Dimensions:</b></td><td><?= $uploadResult["width"] ?? "-" ?> x <?= $uploadResult["height"] ?? "-" ?> px</td></tr> <tr><td><b>MIME Type:</b></td><td><?= $uploadResult["mime_type"] ?></td></tr> </table> <?php endif; ?> <form method="post" enctype="multipart/form-data"> <div class="mb-3"> <label for="image" class="form-label">Select Image (JPG, PNG, GIF, WebP — max 5MB)</label> <input type="file" name="image" id="image" class="form-control" accept="image/*" required> </div> <button type="submit" class="btn btn-primary w-100">Upload Image</button> </form> </div> </div> </div> </div> </div> </body> </html>

6 Multiple File Upload

Create upload_multiple.php — handles uploading multiple images at once:

<?php

require_once __DIR__ . "/classes/FileUploader.php";

$message = "";
$uploadedFiles = [];
$errors = [];

if ($_SERVER["REQUEST_METHOD"] === "POST") {

    if (!isset($_FILES["images"]) || $_FILES["images"]["error"][0] === UPLOAD_ERR_NO_FILE) {
        $message = "Please select at least one image.";
    } else {
        $uploader = new FileUploader([
            "upload_dir"   => "uploads/images",
            "allowed_mime" => FileUploader::IMAGE_TYPES["mime"],
            "allowed_ext"  => FileUploader::IMAGE_TYPES["extensions"],
            "max_size"     => 3 * 1024 * 1024  // 3MB per file
        ]);

        $uploadedFiles = $uploader->uploadMultiple($_FILES["images"]);
        $errors = $uploader->getErrors();

        $successCount = count($uploadedFiles);
        $errorCount   = count($errors);

        if ($successCount > 0) {
            $message = "{$successCount} file(s) uploaded successfully.";
        }
        if ($errorCount > 0) {
            $message .= " {$errorCount} file(s) failed.";
        }
    }
}

?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Multiple File Upload</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
    <div class="container mt-5">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="card shadow-sm">
                    <div class="card-header bg-success text-white">
                        <h4 class="mb-0">Upload Multiple Images</h4>
                    </div>
                    <div class="card-body">

                        <?php if (!empty($message)): ?>
                            <div class="alert alert-info"><?= $message ?></div>
                        <?php endif; ?>

                        <?php if (!empty($errors)): ?>
                            <div class="alert alert-danger">
                                <ul class="mb-0">
                                    <?php foreach ($errors as $error): ?>
                                        <li><?= htmlspecialchars($error) ?></li>
                                    <?php endforeach; ?>
                                </ul>
                            </div>
                        <?php endif; ?>

                        <?php if (!empty($uploadedFiles)): ?>
                            <div class="row mb-3">
                                <?php foreach ($uploadedFiles as $file): ?>
                                    <div class="col-md-4 mb-3">
                                        <div class="card">
                                            <img src="<?= htmlspecialchars($file["path"]) ?>" class="card-img-top" style="height:150px; object-fit:cover;">
                                            <div class="card-body p-2">
                                                <small class="text-muted"><?= htmlspecialchars($file["original_name"]) ?></small><br>
                                                <small><?= $file["size_readable"] ?></small>
                                            </div>
                                        </div>
                                    </div>
                                <?php endforeach; ?>
                            </div>
                        <?php endif; ?>

                        <form method="post" enctype="multipart/form-data">
                            <div class="mb-3">
                                <label for="images" class="form-label">Select Images (max 3MB each, up to 10 files)</label>
                                <input type="file" name="images[]" id="images" class="form-control" accept="image/*" multiple required>
                            </div>
                            <button type="submit" class="btn btn-success w-100">Upload All Images</button>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
</body>
</html>

The key difference for multiple uploads is using name="images[]" with the multiple attribute on the input field, and passing the entire $_FILES["images"] array to uploadMultiple().

7 Document Upload (PDF, DOCX, etc.)

Create upload_document.php — handles document uploads with different validation rules:

<?php

require_once __DIR__ . "/classes/FileUploader.php";

$message = "";
$uploadResult = null;

if ($_SERVER["REQUEST_METHOD"] === "POST") {

    if (!isset($_FILES["document"]) || $_FILES["document"]["error"] === UPLOAD_ERR_NO_FILE) {
        $message = "Please select a document to upload.";
    } else {
        $uploader = new FileUploader([
            "upload_dir"   => "uploads/documents",
            "allowed_mime" => FileUploader::DOCUMENT_TYPES["mime"],
            "allowed_ext"  => FileUploader::DOCUMENT_TYPES["extensions"],
            "max_size"     => 10 * 1024 * 1024  // 10MB for documents
        ]);

        $uploadResult = $uploader->upload($_FILES["document"]);

        if ($uploader->hasErrors()) {
            $message = implode("
", $uploader->getErrors()); } else { $message = "Document uploaded successfully!"; } } } ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document Upload</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"> </head> <body class="bg-light"> <div class="container mt-5"> <div class="row justify-content-center"> <div class="col-md-6"> <div class="card shadow-sm"> <div class="card-header bg-info text-white"> <h4 class="mb-0">Upload Document</h4> </div> <div class="card-body"> <?php if (!empty($message)): ?> <div class="alert alert-<?= $uploadResult ? 'success' : 'danger' ?>"> <?= $message ?> </div> <?php endif; ?> <?php if ($uploadResult): ?> <table class="table table-sm"> <tr><td><b>Original Name:</b></td><td><?= htmlspecialchars($uploadResult["original_name"]) ?></td></tr> <tr><td><b>Saved As:</b></td><td><?= $uploadResult["saved_name"] ?></td></tr> <tr><td><b>Size:</b></td><td><?= $uploadResult["size_readable"] ?></td></tr> <tr><td><b>Type:</b></td><td><?= $uploadResult["mime_type"] ?></td></tr> </table> <?php endif; ?> <form method="post" enctype="multipart/form-data"> <div class="mb-3"> <label for="document" class="form-label">Select Document (PDF, DOC, DOCX, XLS, XLSX, TXT, CSV — max 10MB)</label> <input type="file" name="document" id="document" class="form-control" accept=".pdf,.doc,.docx,.xls,.xlsx,.txt,.csv" required> </div> <button type="submit" class="btn btn-info w-100 text-white">Upload Document</button> </form> </div> </div> </div> </div> </div> </body> </html>
Notice how we reuse the same FileUploader class with different configuration — just change the allowed types, extensions, and max size. This makes the class versatile for any file type.

8 Upload Forms (Bootstrap 5 UI)

Create index.php — a landing page linking to all upload forms:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>PHP File Upload Demo</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
    <div class="container mt-5">
        <div class="text-center mb-5">
            <h1>PHP File Upload Demo</h1>
            <p class="text-muted">Secure file upload with validation</p>
        </div>

        <div class="row justify-content-center">
            <div class="col-md-4 mb-4">
                <div class="card text-center shadow-sm h-100">
                    <div class="card-body">
                        <h5 class="card-title">Single Image</h5>
                        <p class="card-text">Upload one image with preview and validation.</p>
                        <a href="upload_image.php" class="btn btn-primary">Upload Image</a>
                    </div>
                </div>
            </div>
            <div class="col-md-4 mb-4">
                <div class="card text-center shadow-sm h-100">
                    <div class="card-body">
                        <h5 class="card-title">Multiple Images</h5>
                        <p class="card-text">Upload multiple images at once with batch validation.</p>
                        <a href="upload_multiple.php" class="btn btn-success">Upload Multiple</a>
                    </div>
                </div>
            </div>
            <div class="col-md-4 mb-4">
                <div class="card text-center shadow-sm h-100">
                    <div class="card-body">
                        <h5 class="card-title">Documents</h5>
                        <p class="card-text">Upload PDF, Word, Excel, and text files.</p>
                        <a href="upload_document.php" class="btn btn-info text-white">Upload Document</a>
                    </div>
                </div>
            </div>
        </div>
    </div>
</body>
</html>

9 Secure Upload Directory (.htaccess)

Create uploads/.htaccess to prevent script execution inside the upload folder:

# Disable script execution in uploads directory
<FilesMatch "\.(php|phtml|php3|php4|php5|php7|phps|cgi|pl|asp|aspx|shtml|shtm|fcgi|fpl|jsp|htm|html|wml)$">
    Order Allow,Deny
    Deny from all
</FilesMatch>

# Disable directory listing
Options -Indexes

# Only allow access to specific file types
<FilesMatch "\.(jpg|jpeg|png|gif|webp|pdf|doc|docx|xls|xlsx|txt|csv)$">
    Order Deny,Allow
    Allow from all
</FilesMatch>

This is a critical security layer. Even if an attacker manages to upload a PHP file disguised as an image, this .htaccess file prevents Apache from executing it. The file will be served as a download or blocked entirely.

For Nginx servers, add this to your server block:

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

10 Folder Structure

11 Run and Test

Start the PHP development server:

php -S localhost:8000

Open your browser and go to http://localhost:8000 to see the demo landing page.

Test cases to verify:

1. Upload a valid JPG image
→ Should succeed with preview, file info, and dimensions displayed

2. Upload an oversized image (>5MB)
→ Should show "File exceeds the maximum size of 5 MB" error

3. Rename a .php file to .jpg and try uploading
→ Should be rejected — MIME type detection will identify it as text/x-php, not image/jpeg

4. Upload multiple images
→ Should show thumbnails for successful uploads and error messages for rejected files

5. Upload a PDF document
→ Should succeed with file name, size, and MIME type displayed

6. Upload a .exe file
→ Should be rejected — extension not in whitelist

7. Check the uploads directory
→ All files should have randomized names (no original filenames preserved on disk)

12 File Upload Security Checklist

Use this checklist for every PHP file upload implementation in production:

  • Never trust file extensions alone — Always verify the actual MIME type with finfo_file() or the finfo class
  • Never trust $_FILES["type"] — This is sent by the browser and can be spoofed. Always use server-side MIME detection
  • Use an extension whitelist — Only allow explicitly listed extensions. Never use a blacklist (blocking .php, .exe, etc.) as there are too many dangerous extensions to block
  • Rename uploaded files — Never keep the original filename. Use random names to prevent path traversal (../../etc/passwd) and filename collision
  • Disable script execution in uploads directory — Use .htaccess or Nginx config to block PHP execution
  • Set proper file permissions — Uploaded files should be 644 (read/write for owner, read-only for others). Upload directories should be 755
  • Store uploads outside webroot when possible — Serve files through a PHP script that validates access before streaming the file
  • Validate image files with getimagesize() — This confirms the file is a genuine image, not a script with a faked header
  • Scan uploaded content — Check the first few KB of the file for embedded PHP code, script tags, or other dangerous content
  • Enforce file size limits — Both in php.ini and in your application code. This prevents denial-of-service attacks through massive uploads
  • Use HTTPS — Always upload files over encrypted connections to prevent man-in-the-middle attacks
  • Implement rate limiting — Prevent users from flooding your server with upload requests

13 Conclusion

Congratulations! You've built a complete, production-ready file upload system in PHP with security as the top priority. Here's what you've accomplished:

  • A reusable FileUploader class with configurable validation rules
  • Server-side MIME type verification using PHP's finfo extension
  • Extension whitelist and file size enforcement
  • Secure random filename generation to prevent path traversal
  • Image-specific validation (dimensions, genuine image check)
  • Content scanning for embedded malicious code
  • Single and multiple file upload handlers with Bootstrap 5 UI
  • Document upload support (PDF, Word, Excel, CSV)
  • Upload directory protection via .htaccess
  • Comprehensive error handling with user-friendly messages


This FileUploader class is designed to be dropped into any PHP project — whether you're using plain PHP, CodeIgniter, Laravel, or any other framework. Simply adjust the configuration array to match your requirements.

For more advanced features, consider adding image resizing and thumbnail generation with the GD library, virus scanning with ClamAV, cloud storage upload to AWS S3 or Google Cloud Storage, and drag-and-drop uploads with JavaScript.
Revathi M - PHP and CodeIgniter Developer

Written by Revathi M

PHP Developer & Technical Writer · 10+ years building web applications with CodeIgniter and Laravel

Revathi specializes in PHP backend development, authentication systems, and REST API design. She writes practical, production-tested tutorials at Get Sample Code to help developers build secure applications faster.

Frequently Asked Questions

File extensions can be easily spoofed — an attacker can rename malicious.php to malicious.jpg. Always verify the actual file content using PHP finfo functions (MIME type detection) to determine the real file type.

$_FILES["type"] is the MIME type reported by the browser, which can be manipulated by the client. finfo reads the actual file content (magic bytes) on the server to determine the real MIME type. Always use finfo for security.

Edit your php.ini file and set upload_max_filesize (e.g., 10M) and post_max_size (slightly larger, e.g., 12M). Restart your web server after making changes. You can also set these in .htaccess for Apache servers.

Renaming files to random names prevents path traversal attacks (../../etc/passwd), filename collision, and makes it impossible for attackers to guess or directly access uploaded files by their original name.

Yes. The FileUploader class is framework-agnostic. You can use it in any PHP project by including the class file. However, both Laravel and CodeIgniter have their own built-in file upload handlers that integrate with their validation and storage systems.

Add a .htaccess file in your uploads directory that denies access to PHP and other script files. For Nginx, add a location block that denies .php files inside the uploads path. This is a critical security layer for any upload system.