DEV Community

Cover image for How to upload files with PHP correctly and securely
EinLinuus
EinLinuus

Posted on • Edited on

How to upload files with PHP correctly and securely

If you just want the sourcecode - scroll to the end of the page or click here. But I recommend reading the article to understand why I'm doing things as I do and how it works.

Hey Guys,

in this post, I'll show you how to upload files to your server using HTML and PHP and validate the files. I hope it's useful for some of you and now happy coding :)

Security information

First of all, the most important thing I want to tell you, the $_FILES variable in PHP (except tmp_name) can be modified. That means, do not check e.g. the filesize with $_FILES['myFile']['size'], because this can be modified by the uploader in case of an attack. In other words, when you validate the upload with this method, attackers can pretend that their file has another file size or type.

As you can see, there is a lot we need to take care of. Maybe it's worth considering to use an already existing service. With Uploadcare you can upload and manage files quickly and easily via their PHP integration.

So, let's move on and create our own, secure, file upload.

HTML Setup

Before PHP can handle files, we send them to PHP using a basic HTML form:

<form method="post" action="upload.php" enctype="multipart/form-data">
   <input type="file" name="myFile" />
   <input type="submit" value="Upload">
</form>
Enter fullscreen mode Exit fullscreen mode

That's it. Note the action="upload.php", that's the PHP script handling the upload. And we use the name myFile to identify the file in PHP.

PHP Validation

Now, let's validate the file in the upload.php file.

First of all, we have to check if there is a file passed to our script. We do this using the $_FILES variable:

if (!isset($_FILES["myFile"])) {
   die("There is no file to upload.");
}
Enter fullscreen mode Exit fullscreen mode

But remember, for security reasons, we can't get the filesize using $_FILES. When the user uploads the file, PHP stores it temporarily and you can get the path using $_FILES['myFile']['tmp_name']. That's what we use now to get the real size and type of the file.

$filepath = $_FILES['myFile']['tmp_name'];
$fileSize = filesize($filepath);
$fileinfo = finfo_open(FILEINFO_MIME_TYPE);
$filetype = finfo_file($fileinfo, $filepath);
Enter fullscreen mode Exit fullscreen mode

Now we have the real information, let's validate the filesize. We don't want to allow users to upload empty files, so first, we check if the file size is greater than 0:

if ($fileSize === 0) {
   die("The file is empty.");
}
Enter fullscreen mode Exit fullscreen mode

And if anyone can upload files, you might want to set a limit of how large a file can be:

if ($fileSize > 3145728) { // 3 MB (1 byte * 1024 * 1024 * 3 (for 3 MB))
   die("The file is too large");
}
Enter fullscreen mode Exit fullscreen mode

Great. But you'll usually only allow specific types to be uploaded, e.g. .png or .jpg for profile images. For more flexibility, let's create an array with all allowed file types:
(Thanks to Gary Marriott and Renorram Brandão for pointing me out, we have to store the extensions for each type here in the array so we can append it later to the filename)

$allowedTypes = [
   'image/png' => 'png',
   'image/jpeg' => 'jpg'
];
Enter fullscreen mode Exit fullscreen mode

You can find a list of MIME-Types here (It's in german, but there is a great table with all MIME-Types and file extensions).

Now let's check if the type of the file is allowed:

if(!in_array($filetype, array_keys($allowedTypes))) {
   die("File not allowed.");
}
Enter fullscreen mode Exit fullscreen mode

And we're done with validating! In the last step, we move the file to our uploads directory (or wherever you want to). For this, I define a variable with my target directory, then grab the current filename and extension and build the new, target file path:

$filename = basename($filepath); // I'm using the original name here, but you can also change the name of the file here
$extension = $allowedTypes[$filetype];
$targetDirectory = __DIR__ . "/uploads"; // __DIR__ is the directory of the current PHP file

$newFilepath = $targetDirectory . "/" . $filename . "." . $extension;
Enter fullscreen mode Exit fullscreen mode

Finally, move the file:

if (!copy($filepath, $newFilepath )) { // Copy the file, returns false if failed
   die("Can't move file.");
}
unlink($filepath); // Delete the temp file

echo "File uploaded successfully :)";
Enter fullscreen mode Exit fullscreen mode

That's it! Now you have a secure file upload where you can strictly define which files can be uploaded and which not!

Full code

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <!-- Part from this tutorial -->
    <form method="post" action="upload.php" enctype="multipart/form-data">
        <input type="file" name="myFile" />
        <input type="submit" value="Upload">
    </form>
    <!-- End of part from this tutorial -->
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

upload.php:

<?php

if (!isset($_FILES["myFile"])) {
    die("There is no file to upload.");
}

$filepath = $_FILES['myFile']['tmp_name'];
$fileSize = filesize($filepath);
$fileinfo = finfo_open(FILEINFO_MIME_TYPE);
$filetype = finfo_file($fileinfo, $filepath);

if ($fileSize === 0) {
    die("The file is empty.");
}

if ($fileSize > 3145728) { // 3 MB (1 byte * 1024 * 1024 * 3 (for 3 MB))
    die("The file is too large");
}

$allowedTypes = [
   'image/png' => 'png',
   'image/jpeg' => 'jpg'
];

if (!in_array($filetype, array_keys($allowedTypes))) {
    die("File not allowed.");
}

$filename = basename($filepath); // I'm using the original name here, but you can also change the name of the file here
$extension = $allowedTypes[$filetype];
$targetDirectory = __DIR__ . "/uploads"; // __DIR__ is the directory of the current PHP file

$newFilepath = $targetDirectory . "/" . $filename . "." . $extension;

if (!copy($filepath, $newFilepath)) { // Copy the file, returns false if failed
    die("Can't move file.");
}
unlink($filepath); // Delete the temp file

echo "File uploaded successfully :)";
Enter fullscreen mode Exit fullscreen mode

Top comments (6)

Collapse
 
ramriot profile image
Gary Marriott • Edited

I believe you may have left a security hole here.

In the code you first check the file type with finfo_file() & then when you rename the file for permanent storage you carry forward the user provided extension.

This sounds reasonable, but some recent fuzzing I've done on a prominent CMS suggests that on linux servers running php V7 file_info() returns an image mime type for files that it recognises even if the file extension is not.

This would mean that an attacker could potentially craft an arbitrary executable file which would pass mime type image filtering & then be placed where it could be run. In the case of the CMS I was testing when renaming the file I believe they use the inferred mime type to determine the extension.

Collapse
 
renorram profile image
Renorram Brandão

Exactly! Instead of use pathinfo to get the extension, I think the best would be to have an associated array with the "mime_type" => "extension" and use the extension based on the mime type.

Collapse
 
einlinuus profile image
EinLinuus

Thank you both for your comments, I've edited the code like you said and it should be safe now :)

Collapse
 
bigdan256 profile image
BigDan256

Unless php7 or html5 changed things, you may need to specify enctype on the form and a MAX_FILE_SIZE hidden input.
Also you can use is_uploaded_file and move_uploaded_file to ensure you only modify the freshly uploaded file.
You can also use UPLOAD_ERROR_OK, as sometimes a file upload only partially completes.
Lastly, im not sure how to catch, but you can fake a javascript as a gif by putting GIF89 as the first bytes and some other tricks to validate as an image, be careful displaying uploaded content

Collapse
 
kalvaro profile image
Álvaro González

Also you can use is_uploaded_file and move_uploaded_file to ensure you only modify the freshly uploaded file.
You can also use UPLOAD_ERROR_OK, as sometimes a file upload only partially completes.

Exactly! This article made me wonder why they aren't even mentioned when they're the standard way to handle file uploads in PHP.

P.S. Is it possible that the comment form does not even work in Firefox?

Collapse
 
koas profile image
Koas • Edited

Great article! I’d suggest using PHP’s move_uploaded_file and is_uploaded_file functions for extra validation.