09-06-2009

PHP Securing file uploads in php

PHP file upload scripts, if not properly secured pose a big threat to your webserver, as they potentially allow attackers to run any shell command on it. Although it's quite simple to add an image upload script to your site, inexperienced php scripters often tend not to take the security risks to seriously. For a nice explanation of potential risks of file upload scripts have a look at this document (pdf) on php file uploads from scanit.be.

Here's a few precautions that should be taken :

//validate the actual content of the uploaded file to make sure that it is indeed an image
 
$imageinfo = getimagesize($_FILES['userfile']['tmp_name']);
	if($imageinfo['mime'] != 'image/gif' && $imageinfo['mime'] != 'image/jpeg') {
	echo "Sorry, we only accept GIF and JPEG images\n";
	exit;
}
// check the extension with a blacklist (or a whitelist):
 
$blacklist = array(".php", ".phtml", ".php3", ".php4");
foreach ($blacklist as $item) {
	if(preg_match("/$item\$/i", $_FILES['userfile']['name'])) {
		echo "We do not allow uploading PHP files\n";
		exit;
	}
}
// don't allow direct access to uploaded files  - place the outside of the web root
 
$uploaddir = '/var/spool/uploads/'; # Outside of web root
 

image

Also you can store the actual filenames in a database and rename the uploaded file arbitrarily - and only allow acces to the file via it's identifier in the database.

In case the uploaded file is an image you can use a library like GD or Imagick to copy the image information to a new file and discard the original image.

Also care must be taken to not allow attackers to stage denial of service attacks via continuous large file uploads.


Here's an example that incorporates some, though not all of the above.

(Use at your own risk. hehehehe)

<?php
class FileHandler {
	private $userDir			= false;
 
	private $allowedExtensions 	= array('jpg','jpeg','gif','png');
	private $allowedMimes 		= array('image/jpg','image/jpeg','image/gif','image/png'); //requires $allowImagesOnly;
 
	private $renameFiles		= true; //each uploaded file will have a unique id prepended to it's filename
	private $allowImagesOnly	= true;
	private $checkMimes			= true; //requires $allowImagesOnly;
	private $maximumFileSize	= 0;
 
	private $systemErrors = array(
			'noUploaddir'		=>	'Upload directory Does not excist.',
			'noFiles'	=>	'No Files were uploaded.',
			'extension'	=>	'Only the following file types are allowed',
			'imagesOnly'	=> 	'does not appear to be an image file. - please try another file.',
			'mime'		=>	'This file\'s mimetype is not allowed - please try another file',
			'unknown'	=>	'Unknown error uploading file.',
			'maximumFileSize'	=>	'exceeds the maximum file size - '
	);
	private $uploadErrors 		= array(
		    UPLOAD_ERR_INI_SIZE 	=> 	'The uploaded file exceeds the upload_max_filesize directive -- (UPLOAD_ERR_INI_SIZE)',
		    UPLOAD_ERR_FORM_SIZE 	=> 	'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form -- (UPLOAD_ERR_FORM_SIZE)',
		    UPLOAD_ERR_PARTIAL 		=> 	'The uploaded file was only partially uploaded -- (UPLOAD_ERR_PARTIAL)',
		    UPLOAD_ERR_NO_FILE 		=>	'No file was uploaded -- (UPLOAD_ERR_NO_FILE)',
		    UPLOAD_ERR_NO_TMP_DIR 	=> 	'Missing a temporary folder -- (UPLOAD_ERR_NO_TMP_DIR)',
		    UPLOAD_ERR_CANT_WRITE 	=> 	'Failed to write file to disk -- (UPLOAD_ERR_CANT_WRITE)',
		    UPLOAD_ERR_CANT_WRITE 	=> 	'File upload stopped by extension -- (UPLOAD_ERR_CANT_WRITE)'
	);
 
	public function __construct(){
		set_time_limit(360);
		if (!strlen(session_id())) session_start();
	}
 
	public function setAllowedExtensions(Array $extensions){
		$this->allowedExtensions = $extensions; 
	}
 
	public function setCheckMimes($value){//this requires $allowImagesOnly to work;
		$this->checkMimes = $value;
	}
 
	public function setAllowedMimes(Array $allowedMimes){//this requires $allowImagesOnly to work;
		$this->allowedMimes = $allowedMimes; 
	}
 
	public function setRenameFiles($value){
		$this->renameFiles = $value;
	}
 
	public function setAllowImagesOnly($value){
		$this->allowImagesOnly	= $value;
	}
	public function setMaximumFileSize($value){
		$this->maximumFileSize	= $value;
	}
 
	private function checkFiles(){
		$RV= Array('upload'=>array(),'errors'=>array());
		$pattern = '/\.'.implode($this->allowedExtensions,'|\.').'$/i'; 
 
		if (!count($_FILES)){
			$RV['errors']['system']= $this->systemErrors['noFiles'];
			return $RV; 
		}
 
		foreach($_FILES as $key=>$value){
			$filename = $_FILES[$key]['name'] ? $_FILES[$key]['name'] : '';
 
			//php errors 
			if($_FILES[$key]['error']){
				$errorCode = $_FILES[$key]['error'];
				if($errorCode !== UPLOAD_ERR_OK){
			    	if(isset($this->uploadErrors[$errorCode])){
				        $RV['errors'][$key] = $filename.' : '.$this->uploadErrors[$errorCode];
				    } else {
				        $RV['errors']['system'] = $filename.' : '.$this->systemErrors['unknown'];
			    	}
				}
				continue;
			}
 
			/* type check */
			if (!preg_match($pattern,$_FILES[$key]['name'])){
				$RV['errors'][$key] = $filename .' : '.$this->systemErrors['extension'].' : '.implode($this->allowedExtensions,', ') ;
				continue;
			}
 
			if ($this->allowImagesOnly){
				$info = @getimagesize($_FILES[$key]['tmp_name']);
				if(!$info){
					$RV['errors'][$key] = $filename.' : '.$this->systemErrors['imagesOnly'];
				}
				if ($this->checkMimes){
					if(!in_array($info['mime'],$this->allowedMimes)){
						$RV['errors'][$key] = $filename.' : '.$this->systemErrors['mime'] .' : '.implode($this->allowedMimes,', ');
					}
				}
			}
			/* file size */
			if ($this->maximumFileSize){
				if (strstr($this->maximumFileSize,'M')){
					$max = (int)$this->maximumFileSize*1048576;
				}else{
					$max = $this->maximumFileSize;
				}
				if($_FILES[$key]['size'] > $max){
					$RV['errors'][$key] = $filename.' : '.$this->systemErrors['maximumFileSize'].$this->maximumFileSize;
				}
			}	
		}
		return $RV; 
	}
 
	public function saveFilesToUserDir($dir){
		$this->userDir = $dir; 
		$RV = Array();
 
		$RV = $this->checkFiles();
 
		if (!is_dir($this->userDir)){
			$RV['errors']['system']= $this->systemErrors['noUploadDir'];
		}
 
		if (!count($RV['errors'])){
			$_SESSION['fileNames'] = Array();
			foreach($_FILES as $key=>$value){
				//create guid to rename file and write it to session for access 
				$extra = $this->renameFiles ? uniqid().'-' : '';
				$newName = $extra.$_FILES[$key]['name'];
				$uploadfile = $this->userDir.$newName;
 
				if(!array_key_exists($key,$RV)){   // if it hasn't already been invalidated by checkFile
					set_time_limit(360);
					$_SESSION['fileNames'][] = $newName;
					if (move_uploaded_file($_FILES[$key]['tmp_name'],$uploadfile)) {
					  	$RV['upload'][$key] = 1; 
					} 
				}
			}
		}
		if (!count($RV)) $RV['empty'] = 1;
		return json_encode($RV);
	}
}

example initiation:

<?php
require ('FileHandler.php'); 
$fh = new FileHandler();
$result = $fh->saveFilesToUserDir(dirname(__FILE__).'/upload/');
echo $result;
?>

Comments:

Your comment:

»

 

[x]