I don't know if this going to happen, but I will try it.
For past hour I did research on image upload safety. I learned that there a lot of functions to test the upload.
In my project, I need to be safe with images uploaded. There also may be a really big amount of it and it may require a lot of bandwidth, so buying an API is not an option.
So I decided to get a full PHP script for REALLY secure image upload. I also think it will help for many of people out there, because it's impossible to find really secure one. But I am not expert in php, so it's really headache for me to add some functions, so I will ask for this community help to create one full script of REALLY secure image upload.
Really great topics about that are here (however, they are just telling what is needed to do the trick, but not how to do this, and as I said I am not a master on PHP, so I am not able to do this all by myself): PHP image upload security check list https://security.stackexchange.com/questions/32852/risks-of-a-php-image-upload-form
In summary, they are telling that this is what is needed for security image upload (I will quote from the above pages):
- Disable PHP from running inside the upload folder using .httaccess.
- Do not allow upload if the file name contains string "php".
- Allow only extensions: jpg,jpeg,gif and png.
- Allow only image file type.
- Disallow image with two file type.
- Change the image name. Upload to a sub-directory not root directory.
Also:
- Re-process the image using GD (or Imagick) and save the processed image. All others are just fun boring for hackers"
- As rr pointed out, use move_uploaded_file() for any upload"
- By the way, you'd want to be very restrictive about your upload folder. Those places are one of the dark corners where many exploits
happen. This is valid for any type of upload and any programming
language/server. Check
https://www.owasp.org/index.php/Unrestricted_File_Upload- Level 1: Check the extension (extension file ends with)
- Level 2: Check the MIME type ($file_info = getimagesize($_FILES['image_file']; $file_mime = $file_info['mime'];)
- Level 3: Read first 100 bytes and check if they have any bytes in the following range: ASCII 0-8, 12-31 (decimal).
- Level 4: Check for magic numbers in the header (first 10-20 bytes of the file). You can find some of the files header bytes from here:
http://en.wikipedia.org/wiki/Magic_number_%28programming%29#Examples- You might want to run "is_uploaded_file" on the $_FILES['my_files']['tmp_name'] as well. See
http://php.net/manual/en/function.is-uploaded-file.php
Here's a big part of it, but still that's not all. (If you know something more which could help to make the upload even safier, please share.)
THIS IS WHAT WE GOT NOW
Main PHP:
function uploadFile ($file_field = null, $check_image = false, $random_name = false) {
//Config Section
//Set file upload path
$path = 'uploads/'; //with trailing slash
//Set max file size in bytes
$max_size = 1000000;
//Set default file extension whitelist
$whitelist_ext = array('jpeg','jpg','png','gif');
//Set default file type whitelist
$whitelist_type = array('image/jpeg', 'image/jpg', 'image/png','image/gif');
//The Validation
// Create an array to hold any output
$out = array('error'=>null);
if (!$file_field) {
$out['error'][] = "Please specify a valid form field name";
}
if (!$path) {
$out['error'][] = "Please specify a valid upload path";
}
if (count($out['error'])>0) {
return $out;
}
//Make sure that there is a file
if((!empty($_FILES[$file_field])) && ($_FILES[$file_field]['error'] == 0)) {
// Get filename
$file_info = pathinfo($_FILES[$file_field]['name']);
$name = $file_info['filename'];
$ext = $file_info['extension'];
//Check file has the right extension
if (!in_array($ext, $whitelist_ext)) {
$out['error'][] = "Invalid file Extension";
}
//Check that the file is of the right type
if (!in_array($_FILES[$file_field]["type"], $whitelist_type)) {
$out['error'][] = "Invalid file Type";
}
//Check that the file is not too big
if ($_FILES[$file_field]["size"] > $max_size) {
$out['error'][] = "File is too big";
}
//If $check image is set as true
if ($check_image) {
if (!getimagesize($_FILES[$file_field]['tmp_name'])) {
$out['error'][] = "Uploaded file is not a valid image";
}
}
//Create full filename including path
if ($random_name) {
// Generate random filename
$tmp = str_replace(array('.',' '), array('',''), microtime());
if (!$tmp || $tmp == '') {
$out['error'][] = "File must have a name";
}
$newname = $tmp.'.'.$ext;
} else {
$newname = $name.'.'.$ext;
}
//Check if file already exists on server
if (file_exists($path.$newname)) {
$out['error'][] = "A file with this name already exists";
}
if (count($out['error'])>0) {
//The file has not correctly validated
return $out;
}
if (move_uploaded_file($_FILES[$file_field]['tmp_name'], $path.$newname)) {
//Success
$out['filepath'] = $path;
$out['filename'] = $newname;
return $out;
} else {
$out['error'][] = "Server Error!";
}
} else {
$out['error'][] = "No file uploaded";
return $out;
}
}
if (isset($_POST['submit'])) {
$file = uploadFile('file', true, true);
if (is_array($file['error'])) {
$message = '';
foreach ($file['error'] as $msg) {
$message .= '<p>'.$msg.'</p>';
}
} else {
$message = "File uploaded successfully".$newname;
}
echo $message;
}
And the form:
<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post" enctype="multipart/form-data" name="form1" id="form1">
<input name="file" type="file" id="imagee" />
<input name="submit" type="submit" value="Upload" />
</form>
So, what I am asking is to help by posting snippets of codes which will help me (and everyone else) to make this Image Upload Script to make super secure. Or by sharing/creating a full script with all the snippets added.
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 (s)he isn't 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 (s)he wants, 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 (s)he 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 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, (s)he can't access the file on your server directly nor will (s)he be able to fool your server to access the file for him/her as (s)he has no way of knowing which file it is. (S)he 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:
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.