Icerock's Blog Code snippets, useful tips, experiences and a bit of help :o)

1Jun/100

File Upload Validation

Posted by .: Pampa :.

The most (if not every) web developer had to deal with file uploads sometime.

Allowing to put external files in our web server is critic by being a potential attack gateway.
This tutorial is not intended to avoid every kind of possible hack, but it is an intermediate level validation to get rid of errors when the user MUST upload a file and he posts our form without picking one.
Of course we can do this via Javascript but you should know that, a) If our visitor has Javascript disabled on his/her browser we have loosen all control, and b) Every person with basic knowledge of server side scripting could write his own file to post crap to our site.

PHP has many built-in functions to deal with files and even with uploads, but I'll construct a tiered checking logic.
This little logic also checks for the size and the type of the file.

Ok, let's code :)

This will be our very basic posting form to make our tries.

HTML:
  1.     <form action="" method="post" enctype="multipart/form-data">
  2.       <input type="hidden" name="MAX_FILE_SIZE" value="131072" /> <!-- 128*1024 = 128KBytes -->
  3.       <input type="file" name="myfile" />
  4.       <input type="submit"/>
  5.     </form>
  6.   </body>
  7. </html>

And here comes our logic:

PHP:
  1. if(isset($_FILES["myfile"])){
  2.  
  3.   $myfile = $_FILES["myfile"];
  4.  
  5.   // A file exists in the post
  6.   if($myfile["error"]== 4){
  7.     echo(" -- No file uploaded");
  8.   }else{
  9.    
  10.     // No error has occured
  11.     if($myfile["error"]!= 0){
  12.       echo set_error_msg($myfile['error']);
  13.     }else{
  14.      
  15.       // Check if the file was REALLY uploaded
  16.       if( !is_uploaded_file($myfile['tmp_name'] ){
  17.         echo(" -- Sorry, we can't process your file. Please try again.");
  18.       }else{
  19.      
  20.         // Is the kind of file we're expecting?
  21.         if(strpos($myfile["type"], "image/")===false){
  22.           echo(" -- Not a known image type or broken file.");
  23.         }else{
  24.          
  25.           // Is under our expected max file size?
  26.           if($myfile['size']> 1024*128 /*128Kb*/){
  27.             echo(" -- File size exceeded.");
  28.           }else{
  29.             echo "The file is OK! - Going to process :)";
  30.           }
  31.         }      
  32.       }
  33.     }
  34.   }
  35. }

Ok, this seems quite simple, but let's review it step by step:

1) First of all we can't assume there is magically a file posted, or in case it isn't we will receive a warning message by PHP.

2) The first two validations seems a bit redundant, or at least both could be merged into one, but I decided to keep them in that way because of the error handling priority: we can warn the user immediately if no files was uploaded, otherwise then let's check for other possible errors.

3) As I said, other kind of errors could happen, many of them are very uncommon.
Also, I'd recommend you to not expose very PHP-internal errors. More on this a bit later.

4) We should check that the file we're trying to process really came from outside our server, or in other case could mean an attempt to read internal files on our host.

5) A rough file type validation. We can take advantage of the fact that PHP will offer us the ability to get the MIME type of the file. You could simply ask for 'image' in the value to know it's a known image file, whatever its extension, or maybe you could implement a deeper type validation. More on this later too.

6) File size: We can validate a maximum allowed file size. Even if we have a php directive that allows bigger files we maybe expect smaller files.
Tip: Don't rely absolutely on the html directive MAX_FILE_SIZE because I experimented that some browsers override this directive (and remember anyone could post from a hand-crafted script!).
* Note: As far as I know, there's no chance to know in advance the size of the file as for constructing a progress bar or something. The most I found was that there was an ActiveX component that could be plugged in IE to handle this, but only on that browser, not for the rest :(

Of course you would delete those ugly "error messages" and replace them by calling this function:

PHP:
  1. function set_error_msg($errcode){
  2.   // Info
  3.   // http://php.net/manual/en/features.file-upload.errors.php
  4.   //
  5.   //  Err(Int) | Constant              | Description
  6.   // -----------------------------------------------------------
  7.   //      0    | UPLOAD_ERR_OK         | There is no error, the file uploaded with success
  8.   //      1    | UPLOAD_ERR_INI_SIZE   | Exceeded the *upload_max_filesize* directive in php.ini
  9.   //      2    | UPLOAD_ERR_FORM_SIZE  | Exceeds the *MAX_FILE_SIZE* directive in the HTML form
  10.   //      3    | UPLOAD_ERR_PARTIAL    | The uploaded file was only partially uploaded
  11.   //      4    | UPLOAD_ERR_NO_FILE    | No file was uploaded
  12.   //      6    | UPLOAD_ERR_NO_TMP_DIR | Missing a temporary folder (PHP 4.3.10 & 5.0.3)
  13.   //      7    | UPLOAD_ERR_CANT_WRITE | Failed to write file to disk (PHP 5.1.0)
  14.   //      8    | UPLOAD_ERR_EXTENSION  | A PHP extension stopped the file upload (PHP 5.2.0)
  15.   //
  16.   switch ($errcode) {
  17.     case 1:
  18.     case 2:
  19.       return 'File is bigger than the max allowed';
  20.     case 3:
  21.       return 'The file was only partially uploaded';
  22.     case 4:
  23.       return 'No file was uploaded';
  24.     case 6:
  25.     case 7:
  26.     case 8:
  27.       return 'Internal error. Please try again';
  28.     default:
  29.       return 'Unknown upload error';
  30.   }
  31. }

Well, well... this is getting a bit more fancy.

Now, let's talk about the file type validation. What I did above was just to give you an idea, but you can grab more MIME types from w3schools' listing which is a well known source and on this other at webmaster-toolkit.com.

If you do a research you'll find many different types for the same "kind" of file you're expecting.
At least I faced that working with ZIPs.
After a long time of try and fail I ended with this list of possibilities:

PHP:
  1. // Prepare a collection of all posible MIME types for ZIP files
  2. $arrZIPMimes = array("application/zip",
  3.                      "application/x-zip",
  4.                      "application/x-zip-compressed",
  5.                      "application/x-compress",
  6.                      "application/x-compressed",
  7.                      "application/x-gzip-compressed",
  8.                      "application/octet-stream",
  9.                      "multipart/x-zip");

So, depending on what operative system and/or application those files were created could present different MIME.

Ok, you probably are now wondering: Am I going to write a N-tiered elseif control to check for all those possibilities? Don't panic, the short answer is "No".

Loot at this:

PHP:
  1. if( in_array($myfile["type"],$arrZIPMimes) ){ ... }else{ ... }

Easy!, huh? Great!
You could even construct your own file-type-validation function to check for allowed different file types and its MIME in each case.

I'll put all this together in this file so you can download it and see everything working, and also you can play with it.

I hope this piece of code helps you and feel free to leave me your questions / comments.

See you soon,

.: Pampa :.

Filed under: English, PHP No Comments