๐Ÿ“ Web Development

PHP File Upload: Handle $_FILES Safely

Sep 5, 20265 min read0 viewsBy Flow

PHP file upload is how a visitor hands a file to your site โ€” a profile picture, a PDF, a spreadsheet. It builds on PHP forms, with two changes to the form and one new thing to learn on the PHP side: how to take the uploaded file safely, because this is the feature attackers probe first.

The form needs two things it did not before

A normal form cannot carry a file. Two changes fix that: the method must be post, and the form needs enctype="multipart/form-data". Leave the enctype off and the file silently never arrives โ€” no error, just an empty upload, and it is the most common reason "my upload does nothing".

<form method="post" enctype="multipart/form-data">
    <input type="file" name="myfile">
    <button type="submit">Upload</button>
</form>

The type="file" input is the picker. Its name โ€” here myfile โ€” is the key you read on the PHP side, exactly like a normal form field.

What actually arrives: $_FILES

An uploaded file does not land in $_POST. It lands in a separate array, $_FILES, with everything PHP knows about it. Upload something and print it out.

<?php
if ($_SERVER["REQUEST_METHOD"] === "POST" && isset($_FILES["myfile"])) {
    $file = $_FILES["myfile"];

    echo "name:     " . htmlspecialchars($file["name"]) . "<br>";
    echo "type:     " . htmlspecialchars($file["type"]) . "<br>";
    echo "size:     " . $file["size"] . " bytes<br>";
    echo "tmp_name: " . htmlspecialchars($file["tmp_name"]) . "<br>";
    echo "error:    " . $file["error"];
}
?>

<form method="post" enctype="multipart/form-data">
    <input type="file" name="myfile">
    <button type="submit">Inspect</button>
</form>

Output

PHP file upload output showing the five $_FILES values for the chosen image

Five things. name is what the file was called on the visitor's computer. size is its size in bytes. error is 0 when all went well. And tmp_name is the one that matters: PHP has already saved the upload to a temporary location, and this is the path to it. Your job is to move it somewhere permanent before the script ends and PHP throws the temp file away.

Moving the file into place

The plain version: take the temp file and move it into an uploads/ folder. Always use move_uploaded_file(), never copy() โ€” it checks the file really came through an upload and refuses to be tricked into moving something else.

<?php
if ($_SERVER["REQUEST_METHOD"] === "POST" && isset($_FILES["myfile"])) {
    $tmp  = $_FILES["myfile"]["tmp_name"];
    $name = basename($_FILES["myfile"]["name"]);

    if (move_uploaded_file($tmp, "uploads/" . $name)) {
        echo "Uploaded " . htmlspecialchars($name);
    } else {
        echo "Nothing was uploaded.";
    }
}
?>

<form method="post" enctype="multipart/form-data">
    <input type="file" name="myfile">
    <button type="submit">Upload</button>
</form>

Output

PHP file upload output confirming the file moved into the uploads folder

This works, and this is also where nearly every beginner tutorial stops โ€” which is a problem, because the code above trusts the visitor completely. It keeps their filename and accepts any file at all. The next section is the part that actually matters.

The version you would actually ship

Three rules turn the toy above into something safe to put on a real site. Check the size. Check the real type, from the file's own bytes. And store it under a name you generate, never the one the visitor gave.

<?php
$allowed = [
    "image/jpeg" => "jpg",
    "image/png"  => "png",
    "image/webp" => "webp",
];

$maxBytes = 2 * 1024 * 1024; // 2 MB

if ($_SERVER["REQUEST_METHOD"] === "POST" && isset($_FILES["myfile"])) {
    $file = $_FILES["myfile"];

    if ($file["error"] !== UPLOAD_ERR_OK) {
        echo "Upload failed with error code " . $file["error"];
    } elseif ($file["size"] > $maxBytes) {
        echo "Too big. The limit is 2 MB.";
    } else {
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $realType = finfo_file($finfo, $file["tmp_name"]);
        finfo_close($finfo);

        if (!isset($allowed[$realType])) {
            echo "That is a " . htmlspecialchars($realType) . ". Only JPG, PNG and WebP are allowed.";
        } else {
            $ext      = $allowed[$realType];
            $safeName = bin2hex(random_bytes(8)) . "." . $ext;

            if (move_uploaded_file($file["tmp_name"], "uploads/" . $safeName)) {
                echo "Saved as " . htmlspecialchars($safeName);
            } else {
                echo "Could not move the file.";
            }
        }
    }
}
?>

<form method="post" enctype="multipart/form-data">
    <input type="file" name="myfile">
    <button type="submit">Upload</button>
</form>

Output

PHP file upload output showing the file saved under a generated random name

Every line of that guards against something real:

The type check reads the file's bytes, not $_FILES["type"]. That type field is just whatever the browser claimed โ€” an attacker sets it to anything. finfo_file() looks at the actual file to find its true type. Someone renaming evil.php to photo.jpg gets caught here.

The name is generated, not the visitor's. bin2hex(random_bytes(8)) makes an unguessable name and we bolt on the extension we chose. Keep the visitor's filename and someone uploads ../../index.php or a name crafted to overwrite one of your files.

Size is checked before anything else. Without a limit, a big enough file fills your disk.

That is the whole of safe PHP file upload in three habits: reject on size, verify the true type, rename before storing. Skip any one and you have left a door open. Do all three and a PHP file upload is no more dangerous than a text field.

Listing what has been uploaded

Once files are landing, you usually want to see them. scandir() lists a folder.

<?php
$files = array_diff(scandir("uploads"), [".", "..", ".gitkeep"]);

if (!$files) {
    echo "Nothing uploaded yet.";
} else {
    foreach ($files as $file) {
        $path = "uploads/" . $file;
        echo htmlspecialchars($file) . " โ€” " . filesize($path) . " bytes<br>";
    }
}
?>

Output

PHP file upload output listing the uploaded files with their sizes

scandir() also returns . and .., the current and parent folder entries, which you never want in a listing โ€” array_diff() strips them out along with the .gitkeep placeholder.

Three mistakes with PHP file upload

Missing enctype="multipart/form-data". The form submits, but $_FILES is empty. This is the number-one upload bug and it produces no error to point you at it.

Trusting $_FILES["type"] or the visitor's filename. Both come from the browser and both can be faked. Read the real type with finfo, and generate your own filename.

Using copy() or rename() instead of move_uploaded_file(). Only move_uploaded_file() verifies the file genuinely came from an upload, which closes a whole class of trickery.

Common questions about PHP file upload

The questions that come up most the first time a form has to carry a file.

Why is $_FILES empty in PHP?

Almost always the form is missing enctype="multipart/form-data", or its method is not post. Also check the file input has a name and that the file is under the server's upload_max_filesize limit in php.ini.

How do I limit the file type on a PHP upload?

Read the real MIME type from the uploaded file with finfo_file() and compare it against a list you allow. Do not trust $_FILES["type"] or the file extension โ€” both are set by the browser and can be faked.

How do I change the maximum upload size in PHP?

Two settings in php.ini: upload_max_filesize and post_max_size. Raise both, since the POST body has to be big enough to carry the file. Then enforce your own smaller limit in code by checking $_FILES["myfile"]["size"].

Is it safe to keep the uploaded file's original name?

No. A crafted name can overwrite your files or escape the upload folder. Generate your own name โ€” something like bin2hex(random_bytes(8)) plus the extension you decided to allow โ€” and store that instead.

Where does PHP store an uploaded file before I move it?

In a temporary folder, at the path in $_FILES["myfile"]["tmp_name"]. It is deleted automatically when the script finishes, so you must call move_uploaded_file() during that same request to keep it.

What is next

You can now take a file from a visitor and store it safely. Reading and writing files on the server, which this builds on, is covered in PHP file handling.

The natural next step is a small app that lists and opens those files โ€” a file manager. Written safely, it is also a lesson in the single most important rule of touching files by name: building a safe PHP file manager.

Comments

Loading comments...

Link copied to clipboard