So I have an upload script, and I want to check the file type that is being uploaded. I only want pdf, doc, docx and text files
So I have:
$goodExtensions = array('.doc','.docx','.txt','.pdf', '.PDF');
$name = $_FILES['uploadFile']['name'];
$extension = substr($name, strpos($name,'.'), strlen($name)-1);
if(!in_array($extension,$goodExtensions) || (($_FILES['uploadFile']['type'] != "applicatioin/msword") || ($_FILES['uploadFile']['type'] != "application/pdf"))){
$error['uploadFile'] = "File not allowed. Only .doc, .docx, .txt and pdf";
}
Why I'm getting the error when testing and including correct documents?
Since you are using OR instead of AND in your expression:
if (!in_array($extension,$goodExtensions)
|| (($_FILES['uploadFile']['type'] != "applicatioin/msword")
|| ($_FILES['uploadFile']['type'] != "application/pdf"))) {
$error['uploadFile'] = "File not allowed. Only .doc, .docx, .txt and pdf";
}
this always evaluates to true: if the file extension is listed in the array goodExtensions, the first expression is false. However, since the file type can not be both Word and PDF at the same time, the second bracketed expression is always true.
So if you want to ensure that either the file extension or the MIME type is good, the correct expression would be (including the fix for the typo in "applicatioin/msword"):
if (!in_array($extension,$goodExtensions)
|| (($_FILES['uploadFile']['type'] != "application/msword")
&& ($_FILES['uploadFile']['type'] != "application/pdf"))) {
$error['uploadFile'] = "File not allowed. Only .doc, .docx, .txt and pdf";
}
The third parameter for substr is the length, not the end position. If you want everything up until the end of the string just omit the third parameter entirely:
$extension = substr($name, strpos($name,'.'));
You've also spelt application wrong in applicatioin/msword.
Finally, you might want to use strrpos instead of strpos, in case the filename contains other dots before the one separating the extension.
Edit: the logic in the if statement is wrong as well. You error if either the extension isn't known, or the type is not MS Word, or the type is not PDF. The type can't be both of those at once, so it'll always fail. You want the last || to be a &&, I think.
Probably because one (or more) of those 3 conditions in the if statement returns true.
Why I'm getting the error when testing and including correct documents?
I don't know, but you would do well to take the big "if" apart into singular blocks to find the error.
Make test outputs of the MIME type and file extension.
echo "Extension = ".$extension."<br>";
echo "MIME Type = ".$_FILES['uploadFile']['type'];
Also, one thing that jumps the eye is a typo in applicatioin/msword.
Related
I am making an upload script for a music-related thing i am building.
I check whether the file is an MP3 or an OGG, but even if it IS, php will return true on this and say that it isnt on either of these checks.
Here's some example code:
$ft = "mp3";
if($ft != "mp3" || $ft != "ogg") { echo "not an ogg / mp3"; }
What it returns:
not an ogg / mp3
If i am doing something wrong, i am more than glad to be crapped on by Stack Overflow this time, cuz at least i know i did something stupidly wrong.
|| operator means or so if one of the following conditions is true it will do the action of the condition , you need to change it to && which means the both condition should be true to do the action
I have a PHP program which scans a folder named output (which contains all image files in any format) for images. The image files are the screenshots of the output in terminal (I'm using Linux) of various Java programs. The image files are named according to the class name in which the main() function resides. So for example if the main() function resides inside the public Example class, then the output screenshot will be named Example with the extension of either .jpg, .jpeg, .png or .gif. The PHP program has a front page which scans for Java files and lists them in an unordered list with links the user can click to go on the next page. The next page reads the particular file and displays the source code along with the output screenshot. Now since I'm using Fedora and Fedora takes screenshots in png format, that is quite easy like:
echo '<img src="' . $file_name . '".png>'
But what if someone uploads a Java program with the output screenshot in any format? (jpg, jpeg, png, gif). How to then load the appropriate image file since I don't know what the extension will be?
I have an answer to use foreach loop and read through every image file there is within the output folder and then use an if condition for checking the appropriate file names with the various extensions but I think it will not be a very good programming practice.
I generally try to avoid conditions while programming and use more mathematical approach cause that gives me the challenge I need and I feel my code looks different and unique compared to others' but I don't seem to make it work this time.
I'm feeling that this can be done using regular expressions but I don't know how to do it. I know regular expressions but I'm clueless to even how to use them for this. Any answer to not use regular will be appreciated but I want to make this work using regular expressions because in that way I'll also add a little bit of knowledge to my regular expression concepts.
Thanks.
Here's an alternative to MM's that uses RegEx:
function getImageFilename ($basename, $directory) {
$filenames = scandir($directory);
$pattern = "/^" . $basename . "\.(jpeg|png|jpg|gif)$/";
foreach($filenames as $filename) {
preg_match($pattern, $filename, $matches);
if($matches) {
return $filename;
}
}
return false;
}
You can't avoid using a loop. You either loop through the possible file names and check for their existence, or you get a list of all the files in the directory and loop through them whilst performing a pattern match.
If there aren't a lot of files in the directory then this function might perform better because it only needs to call the OS once (to get a list of the files in the directory), whereas asking the OS to check for file existence multiple times requires multiple system calls. (I think that's right...)
One possible solution, you could check if the file exists with that extension (assuming you won't have multiple images with the same name but different extensions):
function get_image($file_name) {
if (file_exists($file_name . '.png')) {
return $file_name . '.png';
} elseif (file_exists($file_name . '.jpg')) {
return $file_name . '.jpg';
} elseif (file_exists($file_name . '.gif')) {
return $file_name . '.gif';
}
return false;
}
echo '<img src="' . get_image($file_name) . '">';
You define the pattern as an or list of the various extensions:
$pattern = '/\.(jpg|png|gif)$/i';
We are also making sure this is an extension by including the match with a dot (escaped) and making sure it's at the end of the string ($). The "i" at the end of that enables case-insensitive matching, so that the regex still picks up GIF or JPG in filenames.
After that, the test is fairly simple:
if (preg_match($pattern, $filename)) {
echo "File $filename is an image";
}
Putting it together in an example, you can see:
$filename = 'test.png';
$pattern = '/\.(jpg|png|gif)$/i';
if (preg_match($pattern, $filename)) {
echo "File $filename is an image";
}
https://eval.in/618651
Whether you want to wrap that in a function, is up to you, as you would have to decide what to return in case the filename does not match one of the extensions provided.
Also note that the test is based on the extension only and not on the content.
// What's my mime?
$_mime = 'text/plain';
if ($_file[strlen($_file)-1] == 'j') { $_mime = 'text/javascript'; }
else { $_mime = 'text/css'; }
I really don't understand why the above does not work, my server will response with two input types either .min.js or .min.css
It should take the last character, step back one, which should either be j or s.
The response is always text/css regardless. Of course strict mime restriction then breaks my entire website.
You're off by one, a common error.
Let's say your filename is script.js.
The length is 9, but since the count in an array starts from 0, the j is the 7th letter, and the s the 8th.
string.js
^ ^
0 8
So just do if ($_file[strlen($_file)-2] == 'j')
$file = 'script.js';
console.log('File name length:', $file.length);
console.log('First letter:', $file[0]);
console.log('Last letter:', $file[$file.length - 1]);
console.log('The letter you want:', $file[$file.length - 2]);
Also, I'm assuming $_file is a string with the name of your file.
Anyway, I hope you're aware you aren't doing a mime-type check, but just checking the file extension - this doesn't provide any security to you, and you cannot be sure about the mime-type of the file. You need to trust the source of the file. So do not use this way to determine the mime-type if the file is uploaded to your server by a third party.
Looks like you are just checking the file extension, in that case you could try a simple function like this to get the extension.
function filename($file){
return substr($file,0,strrpos($file,'.'));
}
function extension($file){
return strtolower(substr(strrchr($file,'.'),1));
}
Note that this function is not bullet proof but in a simple scenario such as your it would work.
A coworker today made a bet with me that he knows of a way to supply a specially formatted string that could pass the following regex check and still supply a file name with extension .php or .jsp or .asp:
if (preg_match('/\.(jpeg|jpg|gif|png|bmp|jpe)$/i', $var) && preg_match('/\.(asp|jsp|php)$/i', $var) == false)
{
echo "No way you have extension .php or .jsp or .asp after this check.";
}
As hard as I tried myself and searched the net, I was unable to find a flaw that would make such thing possible. Could I be overlooking something? Given that "null byte" vulnerability is dealt with, what else might be the issue here?
Note: In no way am I implying that this code is a full-proof method of checking the file extension, there might be a flaw in preg_match() function or the file contents could be of different format, I just ask the question in terms of regex syntax itself.
EDIT - actual code:
if (isset($_FILES["image"]) && $_FILES["image"]["name"] && preg_match('/\.(jpeg|jpg|gif|png|bmp|jpe)$/i', $_FILES["image"]["name"]) && preg_match('/\.(asp|jsp|php)$/i', $_FILES["image"]["name"]) == false) {
$time = time();
$imgname = $time . "_" . $_FILES["image"]["name"];
$dest = "../uploads/images/";
if (file_exists($dest) == false) {
mkdir($dest);
}
copy($_FILES['image']['tmp_name'], $dest . $imgname);
}else{
echo "Invalid image file";
}
PHP version: 5.3.29
EDIT: epilogue
Turned out the 'vulnerability' only presents itself on Windows. Nevertheless, it did exactly what my coworker told me it would - passed the regex check and saved the file with executable extension. Following was tested on WampServer 2.2 with PHP 5.3.13:
Passing the following string to the regex check above test.php:.jpg (note the ":" colon symbol at the end of desired extension) will validate it and the function copy() seems to omit everything after the colon symbol including the symbol itself.
Again, this is only true for windows. On linux the file will be written exactly with the same name as passed to the function.
There is not a single step or a full direct way to exploit your code but here are some thoughts.
You are passing it to copy() in this example but you have mentioned that you have been using this method to validate file ext awhile now so I assume you had other cases that may have used this procedure with other functions too on different PHP versions.
Consider this as a test procedure (Exploiting include, require):
$name = "test.php#.txt";
if (preg_match('/\.(xml|csv|txt)$/i', $name) && preg_match('/\.(asp|jsp|php)$/i', $name) == false) {
echo "in!!!!";
include $name;
} else {
echo "Invalid data file";
}
This will end up by printing "in!!!!" and executing 'test.php' even if it is uploaded it will include it from the tmp folder - of course that in this case you are already owned by the attacker but let's consider this options too.
It's not a common scenario for an uploading procedure but it's a concept that can be exploited by combining several methods:
Let's move on - If you execute:
//$_FILES['image']['name'] === "test.php#.jpg";
$name = $_FILES['image']['name'];
if (preg_match('/\.(jpeg|jpg|gif|png|bmp|jpe)$/i', $name) && preg_match('/\.(asp|jsp|php)$/i', $name) == false) {
echo "in!!!!";
copy($_FILES['image']['tmp_name'], "../uploads/".$name);
} else {
echo "Invalid image file";
}
Again perfectly fine. The file is copied into "uploads" folder - you can't access it directly (since the web server will strip down the right side of the #) but you injected the file and the attacker might find a way or another weak point to call it later.
An example for such execution scenario is common among sharing and hosting sites where the files are served by a PHP script which (in some unsafe cases) may load the file by including it with the wrong type of functions such as require, include, file_get_contents that are all vulnerable and can execute the file.
NULL byte
The null byte attacks were a big weakness in php < 5.3 but was reintroduced by a regression in versions 5.4+ in some functions including all the file related functions and many more in extensions. It was patched several times but it's still out there and alot of older versions are still in use. In case you are handling with an older php version you are definitely Exposed:
//$_FILES['image']['name'] === "test.php\0.jpg";
$name = $_FILES['image']['name'];
if (preg_match('/\.(jpeg|jpg|gif|png|bmp|jpe)$/i', $name) && preg_match('/\.(asp|jsp|php)$/i', $name) == false) {
echo "in!!!!";
copy($_FILES['image']['tmp_name'], "../uploads/".$name);
} else {
echo "Invalid image file";
}
Will print "in!!!!" and copy your file named "test.php".
The way php fixed that is by checking the string length before and after passing it to more deeper C procedure that creates the actual char array and by that if the string is truncated by the null byte (which indicates end of string in C) the length will not match. read more
Strangely enough even in patched and modern PHP releases it's still out there:
$input = "foo.php\0.gif";
include ($input); // Will load foo.php :)
My Conclusion:
Your method of validating file extensions can be improved significantly - Your code allows a PHP file called test.php#.jpg to pass through while it shouldn't. Successful attacks are mostly executed by combining several vulnerabilities even minor ones - you should consider any unexpected outcome and behavior as one.
Note: there are many more concerns about file names and pictures cause they are many time included in pages later on and if they are not filtered correctly and included safely you expose yourself to many more XSS stuff but that's out of topic.
Try this code.
$allowedExtension = array('jpeg','png','bmp'); // make list of all allowed extension
if(isset($_FILES["image"]["name"])){
$filenameArray = explode('.',$_FILES["image"]["name"]);
$extension = end($filenameArray);
if(in_array($extension,$allowedExtension)){
echo "allowed extension";
}else{
echo "not allowed extension";
}
}
preg_match() returns 1 if the pattern matches given subject, 0 if it does not, or FALSE if an error occurred.
$var = "test.php";
if (preg_match('/\.(jpeg|jpg|gif|png|bmp|jpe)$/i', $var) === 1
&& preg_match('/\.(asp|jsp|php)$/i', $var) !== 1)
{
echo "No way you have extension .php or .jsp or .asp after this check.";
} else{
echo "Invalid file";
}
So when you are going to check with your code, use === 1.
Ideally you should use.
function isImageFile($file) {
$info = pathinfo($file);
return in_array(strtolower($info['extension']),
array("jpg", "jpeg", "gif", "png", "bmp"));
}
I remember that in certains version in PHP < 5.3.X, PHP allows strings to contain 0x00, this char is considered as the end of string
So, for exemple, if your string contains : myfile.exe\0.jpg, so preg_match() will match jpg, but other PHP functions will stop in myfile.exe, like include() or copy() functions
I am using Jquery uploader to upload images to my website. It uses a file called uploadhandler.php to manipulate the files. Inside the uploadhandler.php is the following function which appears to make changes to how the filename is formatted etc.
The problem I am having is if I upload a file with spaces in the file name it doesn't appear to be removing the spaces in the file name. Does anyone know how I can edit it to add an extra command to remove any spaces in the file name, or point me in the right direction on how to do it ?.
protected function trim_file_name($name, $type, $index, $content_range) {
// Remove path information and dots around the filename, to prevent uploading
// into different directories or replacing hidden system files.
// Also remove control characters and spaces (\x00..\x20) around the filename:
$file_name = trim(basename(stripslashes($name)), ".\x00..\x20");
// Add missing file extension for known image types:
if (strpos($file_name, '.') === false &&
preg_match('/^image\/(gif|jpe?g|png)/', $type, $matches)) {
$file_name .= '.'.$matches[1];
}
while(is_dir($this->get_upload_path($file_name))) {
$file_name = $this->upcount_name($file_name);
}
$uploaded_bytes = $this->fix_integer_overflow(intval($content_range[1]));
while(is_file($this->get_upload_path($file_name))) {
if ($uploaded_bytes === $this->get_file_size(
$this->get_upload_path($file_name))) {
break;
}
$file_name = $this->upcount_name($file_name);
}
return $file_name;
}
The line:
$file_name = trim(basename(stripslashes($name)), ".\x00..\x20");
Will remove spaces "around" the filename, such as "foo " because of the x20 bit which is a space. You could simply add directly after this line:
$file_name = str_replace(" ", "", $file_name);
Easy as that! Also remember, when you use the 2nd parameter in trim() you remove the "default" list of characters listed in the manual ( http://php.net/trim ) and replace it with your own ...
I would also point out, I would never use the filename as given by the browser in $_FILES[$x]['name'] ... It simply opens too many questions and possibilities up. One technique to avoid the issue altogether might be to simply use the md5() or sha1() or similar of the file's contents, example:
$file_name = md5_file($_FILES['your_file_tag_name']['tmp_name']);
That way in theory, you never ever have spaces in the files, or "dirty" filenames ... And in addition, if a user uploads two files with the precise same contents, you simply need to check for the file's existance, and if it already exists, you already have that exact same file. Hope this helps, this all assumes you're doing file uploads, which I'm not 100% certain you are.