PHP Upload File Enhance Security

php uploading a file security issues?

Resizing the images sounds like a good idea. Don't forget that some real images are uploaded with php code just to use them in any local include vulnerability.

Full Secure Image Upload Script

When you start working on a secure image upload script, there are many things to consider. Now I'm no where near an expert on this, but I've been asked to develop this once in the past. I'm gonna walk through the entire process I've been through here so you can follow along. For this I'm gonna start with a very basic html form and php script that handles the files.

HTML form:

<form name="upload" action="upload.php" method="POST" enctype="multipart/form-data">
Select image to upload: <input type="file" name="image">
<input type="submit" name="upload" value="upload">
</form>

PHP file:

<?php
$uploaddir = 'uploads/';

$uploadfile = $uploaddir . basename($_FILES['image']['name']);

if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadfile)) {
echo "Image succesfully uploaded.";
} else {
echo "Image uploading failed.";
}
?>

First problem: File types

Attackers don't have to use the form on your website to upload files to your server. POST requests can be intercepted in a number of ways. Think about browser addons, proxies, Perl scripts. No matter how hard we try, we can't prevent an attacker from trying to upload something they're not supposed to. So all of our security has to be done serverside.

The first problem is file types. In the script above an attacker could upload anything they want, like a php script for example, and follow a direct link to execute it. So to prevent this, we implement Content-type verification:

<?php
if($_FILES['image']['type'] != "image/png") {
echo "Only PNG images are allowed!";
exit;
}

$uploaddir = 'uploads/';

$uploadfile = $uploaddir . basename($_FILES['image']['name']);

if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadfile)) {
echo "Image succesfully uploaded.";
} else {
echo "Image uploading failed.";
}
?>

Unfortunately this isn't enough. As I mentioned before, the attacker has full control over the request. Nothing will prevent him/her from modifying the request headers and simply change the Content type to "image/png". So instead of just relying on the Content-type header, it would be better to also validate the content of the uploaded file. Here's where the php GD library comes in handy. Using getimagesize(), we'll be processing the image with the GD library. If it isn't an image, this will fail and therefor the entire upload will fail:

<?php
$verifyimg = getimagesize($_FILES['image']['tmp_name']);

if($verifyimg['mime'] != 'image/png') {
echo "Only PNG images are allowed!";
exit;
}

$uploaddir = 'uploads/';

$uploadfile = $uploaddir . basename($_FILES['image']['name']);

if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadfile)) {
echo "Image succesfully uploaded.";
} else {
echo "Image uploading failed.";
}
?>

We're still not there yet though. Most image file types allow text comments added to them. Again, nothing prevents the attacker from adding some php code as a comment. The GD library will evaluate this as a perfectly valid image. The PHP interpreter would completely ignore the image and run the php code in the comment. It's true that it depends on the php configuration which file extensions are processed by the php interpreter and which not, but since there are many developers out there that have no control over this configuration due to the use of a VPS, we can't assume the php interpreter won't process the image. This is why adding a file extension white list isn't safe enough either.

The solution to this would be to store the images in a location where an attacker can't access the file directly. This could be outside of the document root or in a directory protected by a .htaccess file:

order deny,allow
deny from all
allow from 127.0.0.1

Edit: After talking with some other PHP programmers, I highly suggest using a folder outside of the document root, because htaccess isn't always reliable.

We still need the user or any other visitor to be able to view the image though. So we'll use php to retrieve the image for them:

<?php
$uploaddir = 'uploads/';
$name = $_GET['name']; // Assuming the file name is in the URL for this example
readfile($uploaddir.$name);
?>

Second problem: Local file inclusion attacks

Although our script is reasonably secure by now, we can't assume the server doesn't suffer from other vulnerabilities. A common security vulnerability is known as Local file inclusion. To explain this I need to add an example code:

<?php
if(isset($_COOKIE['lang'])) {
$lang = $_COOKIE['lang'];
} elseif (isset($_GET['lang'])) {
$lang = $_GET['lang'];
} else {
$lang = 'english';
}

include("language/$lang.php");
?>

In this example we're talking about a multi language website. The sites language is not something considered to be "high risk" information. We try to get the visitors preferred language through a cookie or a GET request and include the required file based on it. Now consider what will happen when the attacker enters the following url:

www.example.com/index.php?lang=../uploads/my_evil_image.jpg

PHP will include the file uploaded by the attacker bypassing the fact that they can't access the file directly and we're back at square one.

The solution to this problem is to make sure the user doesn't know the filename on the server. Instead, we'll change the file name and even the extension using a database to keep track of it:

CREATE TABLE `uploads` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(64) NOT NULL,
`original_name` VARCHAR(64) NOT NULL,
`mime_type` VARCHAR(20) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
<?php

if(!empty($_POST['upload']) && !empty($_FILES['image']) && $_FILES['image']['error'] == 0)) {

$uploaddir = 'uploads/';

/* Generates random filename and extension */
function tempnam_sfx($path, $suffix){
do {
$file = $path."/".mt_rand().$suffix;
$fp = @fopen($file, 'x');
}
while(!$fp);

fclose($fp);
return $file;
}

/* Process image with GD library */
$verifyimg = getimagesize($_FILES['image']['tmp_name']);

/* Make sure the MIME type is an image */
$pattern = "#^(image/)[^\s\n<]+$#i";

if(!preg_match($pattern, $verifyimg['mime']){
die("Only image files are allowed!");
}

/* Rename both the image and the extension */
$uploadfile = tempnam_sfx($uploaddir, ".tmp");

/* Upload the file to a secure directory with the new name and extension */
if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadfile)) {

/* Setup a database connection with PDO */
$dbhost = "localhost";
$dbuser = "";
$dbpass = "";
$dbname = "";

// Set DSN
$dsn = 'mysql:host='.$dbhost.';dbname='.$dbname;

// Set options
$options = array(
PDO::ATTR_PERSISTENT => true,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
);

try {
$db = new PDO($dsn, $dbuser, $dbpass, $options);
}
catch(PDOException $e){
die("Error!: " . $e->getMessage());
}

/* Setup query */
$query = 'INSERT INTO uploads (name, original_name, mime_type) VALUES (:name, :oriname, :mime)';

/* Prepare query */
$db->prepare($query);

/* Bind parameters */
$db->bindParam(':name', basename($uploadfile));
$db->bindParam(':oriname', basename($_FILES['image']['name']));
$db->bindParam(':mime', $_FILES['image']['type']);

/* Execute query */
try {
$db->execute();
}
catch(PDOException $e){
// Remove the uploaded file
unlink($uploadfile);

die("Error!: " . $e->getMessage());
}
} else {
die("Image upload failed!");
}
}
?>

So now we've done the following:

  • We've created a secure place to save the images
  • We've processed the image with the GD library
  • We've checked the image MIME type
  • We've renamed the file name and changed the extension
  • We've saved both the new and original filename in our database
  • We've also saved the MIME type in our database

We still need to be able to display the image to visitors. We simply use the id column of our database to do this:

<?php

$uploaddir = 'uploads/';
$id = 1;

/* Setup a database connection with PDO */
$dbhost = "localhost";
$dbuser = "";
$dbpass = "";
$dbname = "";

// Set DSN
$dsn = 'mysql:host='.$dbhost.';dbname='.$dbname;

// Set options
$options = array(
PDO::ATTR_PERSISTENT => true,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
);

try {
$db = new PDO($dsn, $dbuser, $dbpass, $options);
}
catch(PDOException $e){
die("Error!: " . $e->getMessage());
}

/* Setup query */
$query = 'SELECT name, original_name, mime_type FROM uploads WHERE id=:id';

/* Prepare query */
$db->prepare($query);

/* Bind parameters */
$db->bindParam(':id', $id);

/* Execute query */
try {
$db->execute();
$result = $db->fetch(PDO::FETCH_ASSOC);
}
catch(PDOException $e){
die("Error!: " . $e->getMessage());
}

/* Get the original filename */
$newfile = $result['original_name'];

/* Send headers and file to visitor */
header('Content-Description: File Transfer');
header('Content-Disposition: attachment; filename='.basename($newfile));
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($uploaddir.$result['name']));
header("Content-Type: " . $result['mime_type']);
readfile($uploaddir.$result['name']);
?>

Thanks to this script the visitor will be able to view the image or download it with its original filename. However, they can't access the file on your server directly nor will they be able to fool your server to access the file for him/her as they has no way of knowing which file it is. They can't brute force your upload directory either as it simply doesn't allow anyone to access the directory except the server itself.

And that concludes my secure image upload script.

I'd like to add that I didn't include a maximum file size into this script, but you should easily be able to do that yourself.

ImageUpload Class

Due to the high demand of this script, I've written an ImageUpload class that should make it a lot easier for all of you to securely handle images uploaded by your website visitors. The class can handle both single and multiple files at once, and provides you with additional features like displaying, downloading and deleting images.

Since the code is simply to large to post here, you can download the class from MEGA here:

Download ImageUpload Class

Just read the README.txt and follow the instructions.

Going Open Source

The Image Secure class project is now also available on my Github profile. This so that others (you?) can contribute towards the project and make this a great library for everyone.

Security threats with uploads

First of all, realize that uploading a file means that the user is giving you a lot of data in various formats, and that the user has full control over that data. That's even a concern for a normal form text field, file uploads are the same and a lot more. The first rule is: Don't trust any of it.

What you get from the user with a file upload:

  • the file data
  • a file name
  • a MIME type

These are the three main components of the file upload, and none of it is trustable.

  1. Do not trust the MIME type in $_FILES['file']['type']. It's an entirely arbitrary, user supplied value.

  2. Don't use the file name for anything important. It's an entirely arbitrary, user supplied value. You cannot trust the file extension or the name in general. Do not save the file to the server's hard disk using something like 'dir/' . $_FILES['file']['name']. If the name is '../../../passwd', you're overwriting files in other directories. Always generate a random name yourself to save the file as. If you want you can store the original file name in a database as meta data.

  3. Never let anybody or anything access the file arbitrarily. For example, if an attacker uploads a malicious.php file to your server and you're storing it in the webroot directory of your site, a user can simply go to example.com/uploads/malicious.php to execute that file and run arbitrary PHP code on your server.

    • Never store arbitrary uploaded files anywhere publicly, always store them somewhere where only your application has access to them.

    • Only allow specific processes access to the files. If it's supposed to be an image file, only allow a script that reads images and resizes them to access the file directly. If this script has problems reading the file, it's probably not an image file, flag it and/or discard it. The same goes for other file types. If the file is supposed to be downloadable by other users, create a script that serves the file up for download and does nothing else with it.

    • If you don't know what file type you're dealing with, detect the MIME type of the file yourself and/or try to let a specific process open the file (e.g. let an image resize process try to resize the supposed image). Be careful here as well, if there's a vulnerability in that process, a maliciously crafted file may exploit it which may lead to security breaches (the most common example of such attacks is Adobe's PDF Reader).


To address your specific questions:

[T]o check even the size of these images I have to store them in my /tmp folder. Isn't it risky?

No. Just storing data in a file in a temp folder is not risky if you're not doing anything with that data. Data is just data, regardless of its contents. It's only risky if you're trying to execute the data or if a program is parsing the data which can be tricked into doing unexpected things by malicious data if the program contains parsing flaws.

Of course, having any sort of malicious data sitting around on the disk is more risky than having no malicious data anywhere. You never know who'll come along and do something with it. So you should validate any uploaded data and discard it as soon as possible if it doesn't pass validation.

What if a prankster gives me a url and I end up downloading an entire website full of malware?

It's up to you what exactly you download. One URL will result at most in one blob of data. If you are parsing that data and are downloading the content of more URLs based on that initial blob that's your problem. Don't do it. But even if you did, well, then you'd have a temp directory full of stuff. Again, this is not dangerous if you're not doing anything dangerous with that stuff.

Security measures when uploading files via php

If all you're doing is acting as a file store, what do you care? You're not executing the files or looking at them in any way, are you?

A file extension has nothing to do with how safe a file is.

Edit: What you would really do is store your filename in a database along with a checksum of the full path. Then your script would be passed that checksum, which would then look up in your database, and then you'd return the file somehow. Don't use your webserver to serve up the file, else (as you say in comments below) you could accidentally execute an uploaded PHP file.

What is the Most Secure php file upload method that can handle all filetypes?

Checking the filename extension is recommended, although be aware that the mime type can easily be spoofed, so this is not a good check for security.

What you have so far is good, my additional tips would be:

  • Virus scan all uploads - this is more to protect other users of your application rather than your server.
  • Store images outside of the web root totally, and use an approach such as this one to proxy the files to be served. This has the advantage that any additional permission checks can be carried out in code (so Bob can't download Alice's files) and as the files are accessed as data, there is no chance of execution.
  • Serve files with the X-Content-Type-Options: nosniff header to prevent any XSS attacks via IE's mime sniffing.
  • Store images using server generated names. For example, you could name each file after the primary key of its database entry. This will mitigate against any directory traversal attacks (like this one) and if a user does manage to upload something malicious they will be less likely to find it on the server.
  • Load all images into an image library and resave them to ensure they don't have any exploits embedded for popular browsers.
  • Have some sort of manual monitoring system in place to watch for any uploaded illegal content.
  • Protect your form against CSRF.

Allowing large file uploads in PHP (security)

The security considerations do not change by changing these settings. However for performance the following is valid:

The art of serving users in a performing way is to offer enough ressources to what is requested by the sum of your users. Translating this into examples upon your settings would be something like:

10 users uploading 950 MB would require you to serve 9.5 GB of bandwidth and I/O throughput (which is eg. ipacted by disk speed) in a performing manner. I as user could probably live with uploading 950 MB in 1 minute, but would be dissatisfied with this taking me an hour.

100 users uploading 950 MB would require you to serve 95 GB...

1000 users uploading 950 MB would reuire you to serve 950 GB...
...

Of cause not all of your users go for max at all the time and even concurrent uploads might be limited. However these Max-settings add to your risk stack. So depending on your usage characteristics and your ressource stuffing these settings could be valid.

However I assume you gave extreme examples and want to learn about implications.

When I google "optimize php memory_limit" I get this:
https://softwareengineering.stackexchange.com/questions/207935/benefits-of-setting-php-memory-limit-to-lower-value-for-specific-php-script

Obviously you can do the same with the other settings.

In forums you can find a lot of swear against setting those config-values such high. However having this in environments, where ressource utilization is managed carefully on other access layers (eg. restrict the number of upload-users via in-app permissions) did work out for me in past very well.

php file upload to the server

The “exif trick” and other measures in that article to sniff file contents are of little use in themselves. (OK, it's worth checking uploaded images are of the expected pixel size, but that's application-specific rather than a security problem.)

The article doesn't say what the threat model is that it's trying to address with filetype sniffing, but what this is commonly trying to do is prevent cross-site scripting attacks, where the attacker includes some active content in the file. Usually this is with HTML in files, which browsers (especially IE) sniff and decide to interpret as HTML even though that's not how the file is being served. Unfortunately, checking that a file begins with a PDF header, or represents a valid GIF image does not help you here because it's possible to make “chameleon” files that can be interpreted as different filetypes simultaneously.

This attack can be blocked in modern browsers by serving the files with a specific non-HTML Content-Type and an X-Content-Type: nosniff header. However there are more obscure attacks involving getting content into Flash or Java plugins that are not affected by this header, and it's not watertight against older browsers.

The really-safe way to stop XSS attacks on uploaded files is simply to serve them from a different hostname (ideally, a different domain name and IP address, but a simple subdomain is at least mostly-effective). Then you can let an attacker XSS the user-uploads-hosting site as much as they like without it having a negative effect on your main site.

Virus scanning is unlikely to prove useful for general-purpose file upload functions. If you are expecting people to use the site to exchange Windows executables then it can be worth scanning those for traditional malware, but for the general case you're typically concerned about attacks against the website itself—server exploitation, XSS, browser exploits—and those kind of attacks are not detected by AV scanners.

Your step (1) of creating a new random filename is a much better approach than “sanitising” user-supplied filenames as the linked article tries to do. Its “safe filename” function is not directly vulnerable to directory traversal, but it does still allow oddnesses like .. (on its own), the empty string, .htaccess, and filenames that would confuse a Windows server, like trailing dots, reserved names and over-long names.

You are right that secure file upload is much trickier than it initially seems, and unfortunately most tutorial code out there (especially for PHP) is pretty disastrous.



Related Topics



Leave a reply



Submit