Full Secure Image Upload Script

PhpImageSecurityFile UploadUpload

Php Problem Overview


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): https://stackoverflow.com/questions/4166762/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.

Php Solutions


Solution 1 - Php

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.

Solution 2 - Php

To upload files in PHP is easy and secure. I would recommend learning about:

To upload a file in PHP you have two methods: PUT and POST. To use the POST method with HTML you need to enable enctype on your form like this:

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

Then in you PHP you need get your uploaded file with $_FILES like this:

$_FILES['file']

Then you need move the file from temp("upload") with move_uploaded_file:

if (move_uploaded_file($_FILES['file']['tmp_name'], YOUR_PATH)) {
   // ...
}

And after you uploaded the file, you need check the file's extension. The best way to do this is using pathinfo like this:

$extension = pathinfo($_FILES['file']['tmp_name'], PATHINFO_EXTENSION);

But the extension is not secure because you can upload a file with extension .jpg but with mimetype text/php and this is a backdoor. So, I recommend checking the real mimetype with finfo_open like this:

$mime = finfo_file(finfo_open(FILEINFO_MIME_TYPE), $_FILES['file']['tmp_name']);

And don't use $_FILES['file']['type'] because sometimes, depending your browser and client OS, you may receive application/octet-stream and this mimetype is not the real mimetype of your uploaded file.

I think you can upload files securely with this scenario.

Sorry for my English, bye!

Solution 3 - Php

here is another tip. Dont rely on the ['type'] element, it is too unreliable. Instead check the file header itself to see what the file type actually is. Something like so:

 <?php


 // open the file and check header

 $tempfile = $FILES['tmp_name'];
                  
 if (!($handle = fopen($tempfile, 'rb')))
 {
   echo 'open file failed';
   fclose($handle);
   exit;

 }else{
  
        $hdr = fread($handle, 12); //should grab first 12 of header
        fclose($handle); 									 
    
 
        //now check the header results
        $subheaderpre = substr($hdr, 0, 12);
        $subheader = trim($subheaderpre);
     
        //get hex value to check png 
        $getbytes = substr($subheader, 0, 8);
        $hxval = bin2hex($getbytes);
     
	 
         if ((substr($subheader, 0, 4) == "\xff\xd8\xff\xe0") && (substr($subheader, 6, 5) == "JFIF\x00"))
         {

           //passed jpg test
 
         }elseif($hxval == "89504e470d0a1a" || substr($subheader, 0, 8) == "\x89PNG\x0d\x0a\x1a\x0a")
           {

              //passed png test 
				
           }else{
			  
                 //fail both 

                 echo 'Sorry but image failed to validate, try another image';
                 exit;                            
					  
                 }//close else elseif else
			  
    }//close else ! $handle 

Solution 4 - Php

Following code work fine for me thanks

      function gen_uid($l=5){
           return substr(str_shuffle("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"), 10, $l);
      }
      $mime_type = mime_content_type($_FILES['imglink']['tmp_name']);
      $allowed_file_types = ['image/png', 'image/jpeg'];
      if (! in_array($mime_type, $allowed_file_types)) {
          // File type is NOT allowed.
        echo "Only PNG and JPG images are allowed!";
        exit;
      }
        $uploaddir = '../uploads/images/';
    
        $name = pathinfo($_FILES['imglink']['name'], PATHINFO_FILENAME);
        $ext  = pathinfo($_FILES['imglink']['name'], PATHINFO_EXTENSION);
    
        $newname = gen_uid(rand(0,30));
        $uploadfile = $uploaddir.$newname.".webp";
        if (move_uploaded_file($_FILES['imglink']['tmp_name'], $uploadfile)) {
            $imglink = basename($uploadfile);
        } else {
            echo "Image upload failed!";
            die();
        }

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionSimonView Question on Stackoverflow
Solution 1 - PhpicecubView Answer on Stackoverflow
Solution 2 - PhpJorge OlafView Answer on Stackoverflow
Solution 3 - PhpdaveView Answer on Stackoverflow
Solution 4 - PhpDarkcoderView Answer on Stackoverflow