Secure PHP File Uploads: The Ultimate Beginner Guide 2026
Secure PHP File Uploads: The Ultimate Beginner Guide 2026

Secure PHP File Uploads: The Ultimate Beginner Guide 20269 min read

  Reading time 13 minutes

Have you ever built a profile page where users can upload an avatar? It feels amazing when it works! But without proper PHP file upload security, that simple form is highly dangerous.

Leaving an upload form unprotected is exactly like leaving your home’s front door wide open. Hackers can easily upload malicious scripts disguised as cute puppy pictures.

If one of those bad files gets into your public folders, an attacker could take full control of your website. We need to build a system that acts like a strict, intelligent bouncer at a club. Let’s write a custom PHP security class step-by-step to keep your server completely safe.

The Danger of Open Doors (PHP File Upload) 🚪

When a normal user uploads a file, your server saves it directly to a folder. If that file is an image, everything is perfectly fine.

But hackers write tiny programs called webshells. A webshell lets the hacker run commands on your server right from their web browser.

Our job is to stop these webshells from ever reaching a public folder. We do this by checking the file type, the size, and the actual raw contents of the upload.

Step 1: The Bouncer’s Clipboard (Rules) 📋

Imagine our bouncer is standing at the door. How does he know what to allow inside? We give him a clipboard with strict rules.

In programming, we define these rules based on the “context” of the upload. A user uploading a tiny profile picture needs completely different limits than someone uploading a massive product video.

Our Upload Rules

  • Profile Image: Images only, maximum 2 Megabytes.
  • Product Video: Video formats only, maximum 50 Megabytes.
  • KYC Document: Images and PDFs, maximum 5 Megabytes.

By setting strict sizes, we stop attackers from crashing our server with gigantic files. We can map these contexts directly in our code.

PHP Memory Management

The Bouncer's Clipboard (Rules)
Secure PHP File Uploads: The Ultimate Beginner Guide 2026
Secure PHP File Uploads: The Ultimate Beginner Guide 2026

Step 2: Checking IDs (Extensions) (PHP File Upload) 🪪

Next, the bouncer needs to check the file’s ID card. The ID card of a file is its extension, like dot-jpg or dot-pdf.

Hackers try to trick basic security guards by using “double extensions.” They name their file something sneaky to bypass simple checks.

A hacker might upload a file named “cute-puppy-dot-php-dot-jpg” hoping the server only looks at the very end. Our security script splits the filename apart and checks every single piece. If we see a dangerous extension anywhere in the name, we throw the file out.

PHP

<?php
// Example of how we check for sneaky extensions
$filename = 'cute-puppy.php.jpg';
$segments = explode('.', $filename);

// We check every part of the name, not just the end!

Step 3: X-Ray Vision (Finding Hidden Code) 🦸‍♂️

Some hackers are even smarter. They create “polyglot” files that act as two things at once.

A polyglot file might look and act exactly like a normal JPEG image. But hidden deep inside the image’s raw data is a dangerous PHP script!

To defeat this, our bouncer needs X-Ray vision. Our script opens the file and reads the first 8000 bytes looking for dangerous code signatures. If we see standard PHP opening tags inside an image, we know it is a trap. (PHP File Upload)

You can read more about how attackers craft these files in the official OWASP File Upload Cheat Sheet.

Step 4: The Quarantine Zone (PHP File Upload) ☣️

What happens when our bouncer catches a bad file? We do not just delete it.

Sometimes, we need to study the attack to understand how the hackers are trying to break in. Instead of deleting the file, we move it to a secure, hidden folder. (PHP File Upload)

This is our Quarantine Zone. We rename the file so it cannot execute, move it safely away from the public, and log the event in our database.

The Quarantine Zone ☣️ Secure PHP File Uploads: The Ultimate Beginner Guide 2026
The Quarantine Zone ☣️ Secure PHP File Uploads: The Ultimate Beginner Guide 2026

6. The Complete Security Code (PHP File Upload)

Here is the complete code for our security guard class. You can save this in your project and use it to inspect every single file before it goes public(PHP File Upload).

PHP

<?php

namespace App\Libraries;

class UploadSecurityGuard
{
    protected const CONTEXTS = [
        'profile_image'     => ['jpg', 'jpeg', 'png', 'webp'],
        'kyc_document'      => ['jpg', 'jpeg', 'png', 'webp', 'pdf'],
        'wallet_proof'      => ['jpg', 'jpeg', 'png', 'webp'],
        'ticket_attachment' => ['jpg', 'jpeg', 'png', 'webp', 'pdf'],
        'product_image'     => ['jpg', 'jpeg', 'png', 'webp'],
        'product_video'     => ['mp4', 'webm', 'mov'],
        'branding_asset'    => ['jpg', 'jpeg', 'png', 'webp'],
    ];

    protected const MAX_BYTES = [
        'profile_image'     => 2 * 1024 * 1024,
        'kyc_document'      => 5 * 1024 * 1024,
        'wallet_proof'      => 5 * 1024 * 1024,
        'ticket_attachment' => 10 * 1024 * 1024,
        'product_image'     => 5 * 1024 * 1024,
        'product_video'     => 50 * 1024 * 1024,
        'branding_asset'    => 2 * 1024 * 1024,
    ];

    protected const DANGEROUS_EXTENSIONS = [
        'php', 'php3', 'php4', 'php5', 'php7', 'php8', 'phtml', 'phar', 'pht',
        'cgi', 'pl', 'py', 'sh', 'exe', 'asp', 'aspx', 'jsp', 'jspx', 'htaccess', 'htpasswd',
    ];

    protected const DANGEROUS_SIGNATURES = ['<?php', '#!/'];
    protected const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp'];
    protected const SIGNATURE_SCAN_BYTES = 8192;

    public function inspectUploadedFile(\CodeIgniter\HTTP\Files\UploadedFile $file, string $context): array
    {
        return $this->inspect($file->getTempName(), $file->getClientName(), $context);
    }

    public function inspect(string $tmpPath, string $originalFilename, string $context): array
    {
        if (! isset(self::CONTEXTS[$context])) {
            return ['ok' => false, 'reason' => "Unknown upload context: {$context}"];
        }

        $extensionCheck = $this->checkExtension($originalFilename, self::CONTEXTS[$context]);
        if (! $extensionCheck['ok']) {
            return $extensionCheck;
        }
        $extension = $extensionCheck['extension'];

        if (! is_file($tmpPath)) {
            return ['ok' => false, 'reason' => 'Uploaded file could not be read.'];
        }

        $size = filesize($tmpPath);
        if ($size === false || $size <= 0) {
            return ['ok' => false, 'reason' => 'Uploaded file is empty or unreadable.'];
        }

        $maxBytes = self::MAX_BYTES[$context];
        if ($size > $maxBytes) {
            return ['ok' => false, 'reason' => 'File exceeds limit.'];
        }

        $contentCheck = $this->checkContent($tmpPath, $extension);
        if (! $contentCheck['ok']) {
            return $contentCheck;
        }

        return ['ok' => true, 'extension' => $extension];
    }

    protected function checkExtension(string $filename, array $allowedExtensions): array
    {
        $segments = explode('.', $filename);
        if (count($segments) < 2) {
            return ['ok' => false, 'reason' => 'File has no extension.'];
        }

        $finalExtension = strtolower((string) end($segments));
        $intermediateSegments = array_slice($segments, 1, -1);

        foreach ($intermediateSegments as $segment) {
            if (in_array(strtolower($segment), self::DANGEROUS_EXTENSIONS, true)) {
                return ['ok' => false, 'reason' => "Disallowed embedded extension found."];
            }
        }

        if (! in_array($finalExtension, $allowedExtensions, true)) {
            return ['ok' => false, 'reason' => "File type not allowed."];
        }

        return ['ok' => true, 'extension' => $finalExtension];
    }

    protected function checkContent(string $tmpPath, string $extension): array
    {
        if (in_array($extension, self::IMAGE_EXTENSIONS, true)) {
            $info = @getimagesize($tmpPath);
            if ($info === false) {
                return ['ok' => false, 'reason' => 'Invalid image.'];
            }
        } elseif ($extension === 'pdf') {
            $header = @file_get_contents($tmpPath, false, null, 0, 5);
            if ($header !== '%PDF-') {
                return ['ok' => false, 'reason' => 'Invalid PDF.'];
            }
        }

        $sample = @file_get_contents($tmpPath, false, null, 0, self::SIGNATURE_SCAN_BYTES);
        if ($sample === false) {
            return ['ok' => false, 'reason' => 'File unreadable.'];
        }
        foreach (self::DANGEROUS_SIGNATURES as $signature) {
            if (stripos($sample, $signature) !== false) {
                return ['ok' => false, 'reason' => "Script signature detected."];
            }
        }

        return ['ok' => true];
    }
}

7. Your Turn to Secure the Web (PHP File Upload)

Securing your server takes a little extra effort, but it is entirely worth it. By checking contexts, sniffing out double extensions, and using X-Ray vision on polyglots, you drastically reduce your attack surface. Taking the time to build a robust guard ensures you can sleep peacefully at night!

What is the sneakiest trick you have ever seen a hacker try to use on an upload form? Leave a comment below, and please share this post with a fellow developer who needs to lock down their server!

5000
0
Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *