Cenzic 232 Patent
Paid Advertising
sla.ckers.org is
ha.ckers sla.cking
Sla.ckers.org
Whether this is about ha.ckers.org, sla.ckers.org or some other project you are interested in or want to talk about, throw it in here to get feedback. 
Go to Topic: PreviousNext
Go to: Forum ListMessage ListNew TopicSearchLog In
Secure upload in PHP
Posted by: Skyphire
Date: May 05, 2011 06:45PM

Hi,

Just completed a script and looking for some feedback/reaction i.e. peer review. The script aims for a high level security when dealing with uploads. Currently it only supports images and runs several in depth scans including a byte-source scan. Securing image uploads is notoriously difficult, due to various attack area's such as: XSS (UTF7, US-ASCII), code injection (PHP etc), RFI/LFI, file confusion, denial of service, resource depletion. All these are dealt within our script.

You can find it here:

http://pastie.org/1869362

It can be run "as is".

Options: ReplyQuote
Re: Secure upload in PHP
Posted by: Skyphire
Date: May 09, 2011 01:00PM

Added a fallback if PNG creation or PNG Alpha is not supported.

Current version:

<?

	function convert($size) {
		$unit=array('b','kb','mb','gb','tb','pb');
		return @round($size/pow(1024,($i=floor(log($size,1024)))),2).' '.$unit[$i];
	}
 
 	function __showmessage() {
		if(isset($_SESSION['upload_message'])) {
			echo "<pre>";
			echo "<strong>Failed.</strong>\r\n";
					foreach($_SESSION['upload_message'] as $message) {
						echo $message . "\r\n" ;
					}
			echo "</pre>";
		}
	}
		
	function __sessionmessage($value) {
		 if(isset($_SESSION['upload_message'])) {
		 	array_push($_SESSION['upload_message'],$value);
			 } else {
			$_SESSION['upload_message'] = array();
		 }
	}
 
	// echo convert(memory_get_usage()) . "\n<hr>";

	function __upload($recv,$path,$thumbnail=false,$wx=false,$hx=false) {
	
		set_time_limit(0);
		session_start();
	
		$sieve 	= 0; // empty sieve
		$slots 	= 50; // maximum number of upload slots for this session.
	
		if(isset($_SESSION['current_slot'])) {
				if($_SESSION['current_slot'] >= $slots) {
					__sessionmessage('Upload slots exceded');
					return FALSE;
					} else {
				$_SESSION['current_slot']++;
				}
			} else {
			$_SESSION['current_slot'] = 1;
		}

		function __scansource($string) {			
			
			$anglematrice = array(
				'3c',
				'c2bc',
				'2575',
				'5c75',
				'253363',
				'26233630',
				'50413d3d',
				'2b4144772d'
			);
		
			$utf7matrice = array(
				'2b4146732d',
				'2b4148302d',
				'2b4148732d',
				'2b4144302d',
				'2b4146302d',
				'2b4144732d',
				'2b4144342d',
				'2b4144772d',
				'2b4143492d',
				'2b4147412d',
				'2b4146382d',
				'2b41434d2d',
				'2b4145412d',
				'2b4143452d',
				'2b4143512d',
				'2b4143552d',
				'2b41436f2d',
				'2b4143592d',
				'2b4146342d',
				'2b4148342d'
	
			);

			$systemmatrice = array(
				 '24485454505f53455353494f4e5f56415253'
				,'24485454505f524551554553545f56415253'
				,'6d6f76655f75706c6f616465645f66696c65'
				,'66696c655f6765745f636f6e74656e7473'
				,'66696c655f7075745f636f6e74656e7473'
				,'24485454505f434f4f4b49455f56415253'
				,'24485454505f434f4f4b49455f56415253'
				,'24485454505f5345525645525f56415253'
				,'6469736b5f746f74616c5f7370616365'
				,'70726f635f6765745f20737461747573'
				,'7365745f66696c655f627566666572'
				,'6469736b5f667265655f7370616365'
				,'6573636170657368656c6c617267'
				,'24485454505f4745545f56415253'
				,'636c656172737461746361636865'
				,'6573636170657368656c6c636d64'
				,'70726f635f7465726d696e617465'
				,'24485454505f454e565f56415253'
				,'70617273655f696e695f66696c65'
				,'6469736b667265657370616365'
				,'75706c6f616465645f66696c'
				,'636f6e74656e742d74797065'
				,'66696c655f657869737473'
				,'65786563757461626c65'
				,'687474702d6571756976'
				,'70726f635f636c6f7365'
				,'7368656c6c5f65786563'
				,'6765745f6c6f61646564'
				,'46494c45494e464f5f'
				,'66696c656d74696d65'
				,'667061737374687275'
				,'667472756e63617465'
				,'777269746561626c65'
				,'66696c657065726d73'
				,'66696c656f776e6572'
				,'66696c6567726f7570'
				,'66696c65696e6f6465'
				,'70726f635f6f70656e'
				,'66696c656174696d65'
				,'66696c656374696d65'
				,'245f53455353494f4e'
				,'736574636f6f6b6965'
				,'70726f635f6e696365'
				,'245f52455155455354'
				,'7661725f64756d70'
				,'73657373696f6e5f'
				,'66696c6573697a65'
				,'726561646c696e6b'
				,'245f534552564552'
				,'7265616466696c65'
				,'70617468696e666f'
				,'7265616c70617468'
				,'66696c6574797065'
				,'7265616461626c65'
				,'6c6f636174696f6e'
				,'66756e6374696f6e'
				,'7061737374687275'
				,'7772697461626c65'
				,'626173656e616d65'
				,'646f63756d656e74'
				,'24474c4f42414c53'
				,'245f434f4f4b4945'
				,'6c696e6b696e666f'
				,'66707574637376'
				,'666e6d61746368'
				,'706870696e666f'
				,'696e695f676574'
				,'696e695f736574'
				,'73796d6c696e6b'
				,'6469726e616d65'
				,'746d7066696c65'
				,'245f46494c4553'
				,'7265706c616365'
				,'7669727475616c'
				,'74656d706e616d'
				,'696e636c756465'
				,'72657175697265'
				,'66676574637376'
				,'6c6368677270'
				,'6c63686f776e'
				,'72656e616d65'
				,'77696e646f77'
				,'657363617065'
				,'636f6f6b6965'
				,'756e6c696e6b'
				,'70636c6f7365'
				,'726577696e64'
				,'24504154485f'
				,'667772697465'
				,'626173653634'
				,'66666c757368'
				,'667363616e66'
				,'64656c657465'
				,'666765747373'
				,'73797374656d'
				,'66636c6f7365'
				,'706870637265'
				,'726d646972'
				,'6674656c6c'
				,'66696c655f'
				,'6667657463'
				,'756d61736b'
				,'63686d6f64'
				,'6368677270'
				,'7768696c65'
				,'63686f776e'
				,'666f637573'
				,'706f70656e'
				,'6d6f757365'
				,'746f756368'
				,'6667657473'
				,'6672656164'
				,'666f70656e'
				,'6670757473'
				,'245f454e56'
				,'6673746174'
				,'666c6f636b'
				,'667365656b'
				,'756e736574'
				,'6d6b646972'
				,'6c73746174'
				,'245f474554'
				,'6576616c'
				,'676c6f62'
				,'6674705f'
				,'7061636b'
				,'65786563'
				,'66696c65'
				,'6d61696c'
				,'6c6f6164'
				,'66656f66'
				,'73746174'
				,'7068705f'
				,'6c696e6b'
				,'6c696e6b'
				,'626c7572'
				,'636f7079'
				,'66696c65'
				,'726567'
				,'646972'
				,'6f625f'
			);
			
			$htmlmatrice = array(
				'6576656e74736f75726365'
				,'626c6f636b71756f7465'
				,'66696763617074696f6e'
				,'7465787461726561'
				,'696e6966696e7479'
				,'6e6f736372697074'
				,'6461746167726964'
				,'646174616c697374'
				,'21646f6374797065'
				,'6b6579626f617264'
				,'6f707467726f7570'
				,'70726f6772657373'
				,'6669656c64736574'
				,'636f6c67726f7570'
				,'636f6d6d616e64'
				,'6973696e646578'
				,'6163726f6e796d'
				,'64657461696c73'
				,'6267736f756e64'
				,'6f7665726c6179'
				,'73656374696f6e'
				,'73756d6d617279'
				,'61727469636c65'
				,'63617074696f6e'
				,'61646472657373'
				,'646f6374797065'
				,'666967757265'
				,'686561646572'
				,'6f7074696f6e'
				,'666f6f746572'
				,'6f626a656374'
				,'696672616d65'
				,'706572736f6e'
				,'737061636572'
				,'6f7574707574'
				,'736372697074'
				,'7374726f6e67'
				,'627574746f6e'
				,'63616e766173'
				,'736f75726365'
				,'6d7374796c65'
				,'6170706c6574'
				,'6867726f7570'
				,'6b657967656e'
				,'73656c656374'
				,'6c6567656e64'
				,'706172616d'
				,'72616e6765'
				,'6c6162656c'
				,'696e707574'
				,'71756f7465'
				,'7468656164'
				,'766964656f'
				,'6d65746572'
				,'6c6162656c'
				,'7374796c65'
				,'6d6f766572'
				,'656d626564'
				,'617564696f'
				,'7469746c65'
				,'74626f6479'
				,'736d616c6c'
				,'74666f6f74'
				,'6173696465'
				,'7461626c65'
				,'73616d70'
				,'7370616e'
				,'72756279'
				,'74696d65'
				,'68656164'
				,'63697465'
				,'636f6465'
				,'61757468'
				,'666f726d'
				,'6d726f77'
				,'626f6479'
				,'61626272'
				,'61726561'
				,'62617365'
				,'6d617271'
				,'6c616e67'
				,'63726564'
				,'6c696e6b'
				,'6d61726b'
				,'6d656e75'
				,'6d657461'
				,'73706f74'
				,'68746d6c'
				,'6d617468'
				,'6e6f7465'
				,'62616e'
				,'776272'
				,'616262'
				,'666967'
				,'766172'
				,'737667'
				,'6b6264'
				,'6e6176'
				,'6d6170'
				,'64666e'
				,'6b6264'
				,'696e73'
				,'737570'
				,'696d67'
				,'64656c'
				,'646976'
				,'737562'
				,'62646f'
				,'636f6c'
				,'707265'
				,'6832'
				,'666e'
				,'6833'
				,'6d6f'
				,'6262'
				,'656d'
				,'646c'
				,'6c68'
				,'6474'
				,'6272'
				,'6464'
				,'6831'
				,'6872'
				,'7472'
				,'7474'
				,'7270'
				,'7468'
				,'7464'
				,'7274'
				,'6834'
				,'756c'
				,'6836'
				,'6835'
				,'215b'
				,'6c69'
				,'6f6c'
				,'212d'
				,'61'
				,'62'
				,'70'
				,'71'
				,'69'
				
			);
					
			$delimiters = array(
				'3f706870'
				,'6a7370'
				,'262378'
				,'262330'
				,'23212f'
				,'40696d'
				,'2f2a'
				,'3c3c'
				,'253d'
				,'2521'
				,'2540'
				,'5c25'
				,'3f3d'
				,'3f2f'
			);
	
			$angl = count($anglematrice);
			$run1 = count($delimiters);
			$run2 = count($htmlmatrice);
			
			if(is_array($string)) {
				array_map('__scansource',$string); // we are recursive.
			} else {
			
			// scan string from a gif, jpg, png or apng (animated png) tmp source.
			// for speed, we only try to find delimiters in the first test.
			if(preg_match("/(3c|c2bc|2575|5c75|253363|26233630|50413d3d|2b4144772d)([a-z-0-9]|5c3f|25|23|5c2b41434d2d|3c|21|5c24|5c2a|40)/mx",$string)) {
			
				// we just found a delimiter, suggesting that we *might* got a malicious file. 
				// this is not certain, because binary data can contain delimiters as mapping.
				// continue for two deep scans to mitigate false positives.
		
						for($i=0; $i<$angl; $i++) {
						
							// run the delimitermatrice
							for($j=0; $j<$run1; $j++) {
								if(stristr($string, $anglematrice[$i].$delimiters[$j]) !== FALSE) {
									// we found something, increment our sieve.
									$sieve++;
									break;
								}
							}
							
							// run the htmlmatrice
							for($k=0; $k<$run2; $k++) {
								if(stristr($string, $anglematrice[$i].$htmlmatrice[$k]) !== FALSE) {
									// we found something, increment our sieve.
									$sieve++;
									break;
								}
							}							
						}
				
						if($sieve >= 1) {
							__sessionmessage('Upload sieve contains malicious data');
							return FALSE; // we got one or more hit. returning.
						} else {
						
						// If were here, were not there yet. let's continue to one final test and inspect unsafe keywords
						// we found delimiters, so something seems wrong. figure out what is.
						
						$sybmbolcount = count($systemmatrice);
						
						for($i=0; $i<$sybmbolcount; $i++) {
							if(stristr($string, $systemmatrice[$i]) !== FALSE) {
								// we found something, increment our sieve.
								$sieve++;
								break;
							}
						}
						
						if($sieve >= 1) {
							// if these symbols are expected, we got some freaky picture going on here.
							// we are now close to 99,99% certain this file is malicious. returning to abort.
							__sessionmessage('Upload sieve contains malicious data');
							return FALSE;
							} else {
							// test passed, continue for resampling.
							return TRUE;
						}
					}
				
					} else {
					// we return, since no delimiter has been found.
					return TRUE;
				}
			}
		}
		
		// allowed image types
		$allowed  	= array(
					'image/gif',
					'image/x-gif',
					'image/agif',
					'image/x-png',
					'image/png',
					'image/a-png',
					'image/apng',
					'image/jpg', 
					'image/jpe', 
					'image/jpeg', 
					'image/pjpeg',
					'image/x-jpeg'
		);
		
		$xpng_mime 	= array(
					'image/x-png',
					'image/png',
					'image/a-png',
					'image/apng'
		);
		
		$jpeg_mime 	= array(
					'image/jpg', 
					'image/jpe', 
					'image/jpeg', 
					'image/pjpeg',
					'image/x-jpeg'
		);
		
		$rmmimes 	= array(
					'.gif',
					'.jpg',
					'.png',
					'.apng'
		);
		
		$normalize	= array(
					'image/gif',
					'image/jpeg',
					'image/png'
		);
		
		// recieved mime
		$recvmime = $_FILES[$recv]['type'];
		
		if(in_array($recvmime,$xpng_mime)) {
			$mime = 'image/png';
			} elseif(in_array($recvmime,$jpeg_mime)) {
			$mime = 'image/jpeg';
			} else {
			$mime = $recvmime; // unknown or gif. push through.
		}
		
		// 1st, check preliminary mime.
		if(!in_array($mime,$allowed)) {
			__sessionmessage('Mimetype not allowed');
			return FALSE;
		}
		
		// check allowed image size.
		if($_FILES[$recv]['size'] > 1000000) {
			__sessionmessage('Upload size ceil reached');
			return FALSE;
		}
		
		// Check image byte markers proceed by parsing the tmp file into a string for inspection.
		$readfile = file_get_contents($_FILES[$recv]['tmp_name']);
		
			if($readfile) {
			
				// it says it's an specific image, let's check if that is true.
				$chunkset = strtolower(bin2hex($readfile)); // normalize
				
					switch($mime) {
					
						// we allow for 16 bit padding
						case $normalize[0]:
							// GIF marker. 
							if(!preg_match("/474946/msx",substr($chunkset,0,16)) && $mime == 'image/gif') {
								__sessionmessage('GIF marker not detected');
								return FALSE;
							}				
						break;
						
						case $normalize[1]:
							//  JFIF header
							if(!preg_match("/ff(d8|d9|c0|c2|c4|da|db|dd)/msx",substr($chunkset,0,16)) && $mime == 'image/jpeg') {
								__sessionmessage('JFIF marker not detected');
								return FALSE;
								// preg_match('/[{0001}-{0022}]/u', $chunkset);
							}
							// JFIF EOF
							if(!preg_match("/ffd9/",substr($chunkset,strlen($chunkset)-32,32))  && $mime == 'image/jpeg') {
								__sessionmessage('JFIF footer incomplete');
								return FALSE;
							}
							// proceed to read exif data, if available.
							if (function_exists('exif_read_data')) {
								$exif = exif_read_data($_FILES[$recv]['tmp_name'], 0, true);
								if($exif["FILE"]["MimeType"] != $mime) {
									__sessionmessage('Mime decrepancy');
									return FALSE;
								}
							}	
						break;
						
						case $normalize[2]:
							// PNG marker
							if(!preg_match("/504e47/",substr($chunkset,0,16)) && $mime == 'image/png') {
								__sessionmessage('PNG marker not detected');
								return FALSE;
							}				
						break;
					}
					
					// do a complete file scan now, since the headers might be faked as well.
					__scansource($chunkset);
				
			} else {
			// we can't read. no good, exit.
			__sessionmessage('Unable to read binary data');
			return FALSE;
		}
		
		// remove extentions
		$safefile = str_replace($rmmimes,'',trim(basename($_FILES[$recv]['name'])));
		
		// sanitize filename
		$safefile = preg_replace('/[^a-zA-Z0-9]/','',$safefile) . '-';

		// entropy for image name.
		$entropy = array(mt_rand(0,0xffff),
				mt_rand(0,0xffff),
				mt_rand(0,0xffff),
				mt_rand(0,0xffff),
				mt_rand(0,0xffff),
				mt_rand(0,0xffff)
		);
		
		shuffle($entropy);
				
		// create UUID
		$safefile .= $entropy[0] .'-';
		$safefile .= $entropy[1] .'-';
		$safefile .= $entropy[2]; 
		
		list($width, $height) = getimagesize($_FILES[$recv]['tmp_name']);
		
		// if thumbnail, get new size.
		if($thumbnail == true) {
			$new_width  = $wx;
			$new_height = $hx;
			} else {
			$new_width  = $width;
			$new_height = $height;
		}
		
		// check if GD is available
		
		$gdcheck 	= gd_info();
		$gd 		= TRUE;
		
		// resample.
		switch($mime) {
		
				case $normalize[0]:
				
				if($gdcheck["GIF Create Support"] == true) {
					$ext = '.gif';
					$image = imagecreatefromgif($_FILES[$recv]['tmp_name']);
					$resampled = imagecreatetruecolor($new_width, $new_height);
					imagecopyresampled($resampled, $image, 0,0,0,0, $new_width, $new_height, $width, $height);
					imagegif($resampled, $path . $safefile . $ext);
					$endsize = filesize($path . $safefile . $ext);
					
					} else {
					$gd = FALSE;
				}
							
				
				break;
				
				case $normalize[1]:
				
				if($gdcheck["JPG Support"] == true || $gdcheck["JPEG Support"] == true) {
					$ext = '.jpg';
					$image = imagecreatefromjpeg($_FILES[$recv]['tmp_name']);
					$resampled = imagecreatetruecolor($new_width, $new_height);
					imagecopyresampled($resampled, $image, 0,0,0,0, $new_width, $new_height, $width, $height);
					imagejpeg($resampled, $path . $safefile . $ext, 100);
					$endsize = filesize($path . $safefile . $ext);
						
					} else {
					$gd = FALSE;
				}
				
				break;
				
				case $normalize[2]:
				
				if($gdcheck["PNG Support"] == true) {
					$ext = '.png';
					$resampled = imagecreatetruecolor($new_width, $new_height);
					$image = imagecreatefrompng($_FILES[$recv]['tmp_name']);
					imagealphablending($image, true); 
					imagesavealpha($image, true); 
					imagecopyresampled($resampled, $image, 0,0,0,0, $new_width, $new_height, $width, $height);
					imagepng($resampled, $path . $safefile . $ext, 100);
					$endsize = filesize($path . $safefile . $ext);
	
					} else {
					$gd = FALSE;
				}	
				
				break;
				
			default:
			__sessionmessage('Unsupported format');
			return FALSE;
		}
		
		if($gd == FALSE || $endsize == 0) {
			move_uploaded_file($_FILES[$recv]['tmp_name'], $path . $safefile . '-verbatim-'. $ext);
			__sessionmessage('GD unsupported, but image processed.');
			return TRUE;
			} else {
			imagedestroy($resampled);
			return TRUE;
		}		
	}

	if($_REQUEST['upload']) {

	__upload('files',"test/",false,false,false);
	
	// show messages, if available:
	__showmessage();

	}
    
?>

<form name="" action="" method="post" enctype="multipart/form-data">
<input type="file" name="files"  />
<input type="hidden" name="upload" value="1" />
<input type="submit" name="submit" />
</form>

// don't forget to create a /test/ folder to write files to.

btw; the matrice arrays contain vectors in hex format which will be compared against a hex conversion of the submitted image.

I'm interested in your opinions, not about the way I code it, but about the mechanisms involved and possible overlooked flaws or bottlenecks. Processing might take a bit longer than simply copying the image but that is expected.



Edited 3 time(s). Last edit at 12/20/2011 08:33AM by Skyphire.

Options: ReplyQuote
Re: Secure upload in PHP
Posted by: xqus
Date: December 19, 2011 06:52PM

Interesting indeed. I have been looking for something like this for quite a while now. Would it be possible to use this in a open source project (MIT license)?

Options: ReplyQuote
Re: Secure upload in PHP
Posted by: Skyphire
Date: December 20, 2011 08:37AM

Sure, no problem it hereby is GPLv3.

I just spotted and updated the above post with a minor fix in thumbnail creation. I also noticed that on some PHP versions that mt_rand() acts as a constant, So adding additional randomness to the filename (so no-one can guess it's location) might be another idea.

Options: ReplyQuote
Re: Secure upload in PHP
Posted by: Skyphire
Date: December 20, 2011 08:42AM

Here is the latest tested version.

it also returns the location of the stored image: http://pastie.org/3047101

Options: ReplyQuote


Sorry, only registered users may post in this forum.