If you want users to upload files to your server you need to do a couple of security checks before you actually move the uploaded file to your web directory.
This array contains user submitted data and is not information about the file itself. While usually this data is generated by the browser one can easily make a post request to the same form using software.
$_FILES['file']['name'];
$_FILES['file']['type'];
$_FILES['file']['size'];
$_FILES['file']['tmp_name'];
name
- Verify every aspect of it.type
- Never use this data. It can be fetched by using PHP functions instead.size
- Safe to use.tmp_name
- Safe to use.Normally the operating system does not allow specific characters in a file name, but by spoofing the request you can add them allowing for unexpected things to happen. For example, lets name the file:
../script.php%00.png
Take good look at that filename and you should notice a couple of things.
../
, fully illegal in a file name and at the same time perfectly fine if you are moving a file from 1 directory to another, which we're gonna do right?%00
to a null
character, basically saying to the operating system, this string ends here, stripping off .png
off the filename.So now I've uploaded script.php
to another directory, by-passing simple validations to file extensions. It also by-passes .htaccess
files disallowing scripts to be executed from within your upload directory.
You can use pathinfo()
to extrapolate the name and extension in a safe manner but first we need to replace unwanted characters in the file name:
// This array contains a list of characters not allowed in a filename
$illegal = array_merge(array_map('chr', range(0,31)), ["<", ">", ":", '"', "/", "\\", "|", "?", "*", " "]);
$filename = str_replace($illegal, "-", $_FILES['file']['name']);
$pathinfo = pathinfo($filename);
$extension = $pathinfo['extension'] ? $pathinfo['extension']:'';
$filename = $pathinfo['filename'] ? $pathinfo['filename']:'';
if(!empty($extension) && !empty($filename)){
echo $filename, $extension;
} else {
die('file is missing an extension or name');
}
While now we have a filename and extension that can be used for storing, I still prefer storing that information in a database and give that file a generated name of for example, md5(uniqid().microtime())
+----+--------+-----------+------------+------+----------------------------------+---------------------+
| id | title | extension | mime | size | filename | time |
+----+--------+-----------+------------+------+----------------------------------+---------------------+
| 1 | myfile | txt | text/plain | 1020 | 5bcdaeddbfbd2810fa1b6f3118804d66 | 2017-03-11 00:38:54 |
+----+--------+-----------+------------+------+----------------------------------+---------------------+
This would resolve the issue of duplicate file names and unforseen exploits in the file name. It would also cause the attacker to guess where that file has been stored as he or she cannot specifically target it for execution.
Checking a file extension to determine what file it is is not enough as a file may named image.png
but may very well contain a php script. By checking the mime-type of the uploaded file against a file extension you can verify if the file contains what its name is referring to.
You can even go 1 step further for validating images, and that is actually opening them:
if($mime == 'image/jpeg' && $extension == 'jpeg' || $extension == 'jpg'){
if($img = imagecreatefromjpeg($filename)){
imagedestroy($img);
} else {
die('image failed to open, could be corrupt or the file contains something else.');
}
}
You can fetch the mime-type using a build-in function or a class.
Most importantly, you should whitelist file extensions and mime types depending on each form.
function isFiletypeAllowed($extension, $mime, array $allowed)
{
return isset($allowed[$mime]) &&
is_array($allowed[$mime]) &&
in_array($extension, $allowed[$mime]);
}
$allowedFiletypes = [
'image/png' => [ 'png' ],
'image/gif' => [ 'gif' ],
'image/jpeg' => [ 'jpg', 'jpeg' ],
];
var_dump(isFiletypeAllowed('jpg', 'image/jpeg', $allowedFiletypes));