A very overlooked part of PHP security is file uploading. A quick for instance. Lets say that you have a file uploader in which there is no security applied. Any file may be uploaded to the server. What if someone uploads a file that has a little extension by the name of ‘.php’? All that needs to be done now is in the file write a little script with ‘fopen’ and read all the files and echo them out to the browser. Now we have all the source code (maybe even usernames/passwords if we’re lucky) to the entire application. Not only that, but now we can write sql code to get all the data we want from the database. Then we could get really mean and update the database with some malicious javascript and have it download viruses or trojans on all the users computers who visit the website. Now google checks your website, realizes something bad is going on and decides to blacklist your website. Now mr. user comes along to your website, and he’s using firefox. Firefox checks with google to see if its ok to visit the website, google says ‘noooo’, firefox puts up a really scary red screen with a hand and an exclamation point and mr. user gets scared and decides never to come to this ‘bad’ site again. Then russia decides to launch ‘nucular’ missiles and we’re back in the cold war…. ok,, maybe not that last part but pretty much all the stuff before ‘nucular’ missiles I’ve experienced happening. So how do we stop the madness??
Stopping the madness
… by taking security seriously!!!! I can’t emphasize this enough. Hacking simply isn’t perpetrated by elite geniuses (they like to think their geniuses), it is perpetrated by every day people (alot of times kids) who find a weak spot in a web application or website and spend massive amounts of time exploiting it. So what do we need to know about file upload security? Well first, we need to stop any files that can execute commands of any kind. If your website is located on a shared server typically the server supports many different languages such as PHP, Perl, Python, Ruby and so on. We need to make a ‘black list’ of the files we absolutely do NOT want on the server. Next we need to know what types of files we want to allow and what mime types we want to allow. So basically a white list of files that we are allowing to be uploaded. The next thing to think about is file size. Since we live in a world of scarcity (my economics professor would be proud) we must manage space. Space on a server is a commodity and files uploaded by users must be ‘rationed’ (again, my econ prof would be proud lol).
The Code
So how exactly do we do this? Here is the code:
<?php class file_upload { private $uploaddir; private $uploadfile; private $maxfilesize; /** * The results returned in this message * * @var string */ public $message; /** * Upload a file or files to the server * * @param string $upload_dir * @param string $upload_file * @param string $max_file_size */ public function __construct($upload_dir, $upload_file, $max_file_size = '') { if (strpos($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST']) === false) { $this->message = 'Request must originate from domain'; return false; } $this->uploaddir = $upload_dir; $this->uploadfile = $upload_file; $this->maxfilesize = $max_file_size; if ($this->maxfilesize == '') { $this->maxfilesize = config::max_file_size; } ini_set('max_file_size', $this->maxfilesize); } /** * Upload files * * @param array $white_list * @param string $black_list */ public function upload($white_list, $black_list = '') { for ($i = 0; $i < count($_FILES[$this->uploadfile]['name']); $i++) { $uploadErrors = array( UPLOAD_ERR_INI_SIZE => 'The uploaded file exceeds the upload_max_filesize directive in php.ini.', UPLOAD_ERR_FORM_SIZE => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.', UPLOAD_ERR_PARTIAL => 'The uploaded file was only partially uploaded.', UPLOAD_ERR_NO_FILE => 'No file was uploaded.', UPLOAD_ERR_NO_TMP_DIR => 'Missing a temporary folder.', UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk.', UPLOAD_ERR_EXTENSION => 'File upload stopped by extension.', ); if ($_FILES[$this->uploadfile]['error'][$i] != 0) { $this->message = $uploadErrors[$_FILES[$this->uploadfile]['error'][$i]]; return false; } if(is_executable($_FILES[$this->uploadfile]['tmp_name'][$i])) { $this->message = "Uploading executable files Not Allowed"; return false; } if ($black_list == '') { $black_list = explode(",", config::file_black_list); } else { $black_list = explode(",", $black_list); } $black_listed = false; foreach ($black_list as $file) { if (preg_match("/$file\$/i", $_FILES[$this->uploadfile]['name'][$i])) { $black_listed = true; } } if ($black_listed == true) { $this->message = 'This file type has been blacklisted'; return false; } if ($white_list != '') { $mime = ''; $white_listed = false; foreach ($white_list as $file => $mime) { if (preg_match("/$file\$/i", $_FILES[$this->uploadfile]['name'][$i]) && $mime == $_FILES[$this->uploadfile]['type'][$i]) { $white_listed = true; break 1; } } if ($white_listed == false) { $this->message = 'This type of file is not allowed'; return false; } } if ($_FILES[$this->uploadfile]['size'][$i] > $this->maxfilesize) { $this->message = 'You cannot upload files this size. The maximum file size is ' . $this->maxfilesize; return false; } if (!is_uploaded_file($_FILES[$this->uploadfile]['tmp_name'][$i])) { $this->message = 'The file is not an uploaded file'; return false; } $uploadfile = realpath($this->uploaddir) . (strpos($this->uploaddir, '\\') !== false ? '\\' : '/') . basename($_FILES[$this->uploadfile]['name'][$i]); if (move_uploaded_file($_FILES[$this->uploadfile]['tmp_name'][$i], $uploadfile)) { $this->message = 'The file has been successfully uploaded'; return true; } else { return false; } } } } ?>
Here’s what would go in the config file:
class config { // Upload Const max_file_size = 9000000; Const file_black_list = '.php,.js,.py'; }
Here is the file which will process the form:
<?php if ($_SERVER['REQUEST_METHOD'] == 'POST') { $uploader = new file_upload(".", 'files'); $whitelist = array('jpg' => 'image/jpeg', 'gif' => 'image/gif'); $uploader->upload($whitelist); } ?>
Here is the html form:
<table width="500" border="0" align="center" cellpadding="0" cellspacing="1" bgcolor="#cccccc"> <tr> <form action="uploader.php" method="POST" enctype="multipart/form-data" name="form1" id="form1"> <td> <table width="100%" border="0" cellpadding="3" cellspacing="1" bgcolor="#cccccc"> <tr> <td> <label for="files">Select File</label> </td> <td> <input name="files[]" type="file" id="files" /></td> </tr> <tr> <td align="center"><input type="submit" name="Submit" value="Upload" /></td> </tr> </table> </td> </form> </tr> </table>
So how does this work? First we use the is_executable function to check to make sure that the file isn’t registered on the server to be an executable as this could be dangerous. It could be uploaded and then started by a php script using ‘exec()’. Then we setup a blacklist that defines files that should never be uploaded. I’ve defined a few of them but there are many more. Next, moving along through the function, we then create a whitelist in which we define the file extension and the mime type in which we will allow to upload. Then we check the size of the uploaded file and ensure the size is not larger than we defined. The next thing we check is to make sure the file we are manipulating is a file that has been uploaded from the client. If that file is a valid uploaded file then we move the uploaded file from the temporary directory to the designated location.
Conclusion
Uploading files can be an easy process. Uploading files securely and protecting against malicious attacks can be a bit more complex but believe me when I say it is worth the extra work to protect yourself in the beginning and in the end you save yourself alot more time, hassle and headache.

January 27th, 2009 at 8:54 pm
Wow! Thank you very much!
I always wanted to write in my blog something like that. Can I take part of your post to my blog?
Of course, I will add backlink?
Regards, Timur Alhimenkov
January 28th, 2009 at 10:16 am
Sure. Spread the knowledge.