Skip to content

PHP Tutorial File Handling

Secure File Uploads

Lesson 174 of 369 2 min read By Deepak Paswan Updated Oct 5, 2026

An upload form lets strangers put files on your server. A careless one can hand over the whole site. Follow these rules every time.

The rules

  1. Check the upload error code.
  2. Limit the size.
  3. Check the real content type, not the file name or the browser’s type value.
  4. Allow only the types you need (an allow-list).
  5. Never use the visitor’s file name. Generate your own.
  6. Store files where PHP cannot run, ideally outside the public folder.
  7. Serve them safely, with the correct headers.

A validation function

This function checks what it can before the file is moved. We test it with real temporary files: one genuine PNG image and one PHP script that has been renamed to photo.jpg.

index.php
PHP
<?php
function validateImageUpload(array $file, int $maxBytes = 2_000_000): ?string
{
    if (($file["error"] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
        return "The upload failed.";
    }

    if (($file["size"] ?? 0) > $maxBytes) {
        return "The file is too large.";
    }

    $finfo = new finfo(FILEINFO_MIME_TYPE);
    $mime = $finfo->file($file["tmp_name"]);

    $allowed = ["image/jpeg", "image/png", "image/webp"];

    if (!in_array($mime, $allowed, true)) {
        return "Only JPG, PNG and WebP images are allowed.";
    }

    if (getimagesize($file["tmp_name"]) === false) {
        return "The file is not a valid image.";
    }

    return null;   // null means "no problem found"
}

$png = base64_decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==");
file_put_contents("upload.tmp", $png);
file_put_contents("script.tmp", "<?php echo 'evil';");

$goodFile = ["name" => "x.png", "error" => 0, "size" => filesize("upload.tmp"), "tmp_name" => "upload.tmp"];
$badFile = ["name" => "photo.jpg", "error" => 0, "size" => filesize("script.tmp"), "tmp_name" => "script.tmp"];

var_dump(validateImageUpload($goodFile));
var_dump(validateImageUpload($badFile));
Output
OUTPUT
NULL
string(42) "Only JPG, PNG and WebP images are allowed."

The file named photo.jpg was rejected, because finfo looked at its bytes and saw a script, not an image.

Choose the extension yourself

Never take the extension from the visitor’s file name. Choose it from the detected type.

index.php
PHP
<?php
function extensionFor(string $mime): ?string
{
    return ["image/jpeg" => "jpg", "image/png" => "png", "image/webp" => "webp"][$mime] ?? null;
}

var_dump(extensionFor("image/png"));
var_dump(extensionFor("text/x-php"));
Output
OUTPUT
string(3) "png"
NULL

Generate the file name

A random name stops guessing, overwriting and tricks such as shell.php.jpg or ../../index.php.

index.php
PHP
<?php
$name = bin2hex(random_bytes(16)) . ".png";

echo strlen($name);
Output
OUTPUT
36

Save the file

index.php
PHP
<?php
$error = validateImageUpload($_FILES["photo"] ?? []);

if ($error !== null) {
    exit($error);
}

$mime = (new finfo(FILEINFO_MIME_TYPE))->file($_FILES["photo"]["tmp_name"]);
$name = bin2hex(random_bytes(16)) . "." . extensionFor($mime);
$target = dirname(__DIR__) . "/storage/uploads/" . $name;   // outside the public folder

if (!move_uploaded_file($_FILES["photo"]["tmp_name"], $target)) {
    exit("Could not save the file.");
}

// store $name in the database, not the visitor's original name

Where to store the files

  • Best: outside the public web folder. PHP reads and sends the file through a script that checks permissions.
  • If the folder must be public (for example for profile pictures), block code execution there. On Apache, put this in .htaccess inside the uploads folder:
code
APACHE
php_flag engine off
RemoveHandler .php .phtml .php5
<FilesMatch "\.(php|phtml|phar)$">
    Require all denied
</FilesMatch>

An uploaded .php file must never be runnable from the browser.

Serve downloads safely

index.php
PHP
<?php
$file = dirname(__DIR__) . "/storage/uploads/" . basename($storedName);

if (!is_file($file)) {
    http_response_code(404);
    exit;
}

header("Content-Type: image/png");
header("X-Content-Type-Options: nosniff");
header("Content-Length: " . filesize($file));
readfile($file);

basename() strips any folder parts. nosniff stops browsers from guessing a different type.

Extra protection for images

Re-encoding an image with GD removes anything hidden inside it, such as a script appended to the end of a valid image.

index.php
PHP
<?php
$image = imagecreatefromstring(file_get_contents($tmpName));
imagepng($image, $target);   // writes a clean PNG
imagedestroy($image);

Checklist

  • Error code checked
  • Size limit in PHP and in php.ini / the web server
  • Real type checked with finfo
  • Allow-list of types and extensions
  • Random file name, extension chosen by you
  • Stored outside the web root, or with execution disabled
  • Only logged-in users may upload, and you limit how many
  • Original file name (if you show it) is escaped on output
dangerous
What not to trust
$_FILES["x"]["name"], $_FILES["x"]["type"] and the extension are written by the sender. Only the content, checked by PHP, tells you what the file is.