Why doesn't my simple filename extension check work? - php

// 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.

Related

PHP regex vulnerability bet

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

Trouble uploading files with special characters

I have coded a little upload script that works just fine in every case when I use it, but I always get messages from my client that he gets an error that I die() when the upload goes wrong/doesn't work.
I believe that this was caused by the special characters they use. Because they are Austrians they have special chars like äÄöÖüÜß in their filenames. Plus they use whitespaces in there filenames.
Therefore I used a regex to replace special characters into underlines(_).
$moveFile = function($tmpname,$name,$time) {
// the regex to resolve the special chars problem
$name = preg_replace('/[^a-zA-Z0-9\.]/s', '_', $name);
if (!move_uploaded_file($tmpname,'assets/siteContents/bewerbungen/'.$time.'_'.$name)) {
die('something went wrong while uploading');
}
};
// move application_files__image
$moveFile($_FILES['application_files__image']['tmp_name'],
$_FILES['application_files__image']['name'],
$time);
// move application_files__image
$moveFile($_FILES['application_files__cv']['tmp_name'],
$_FILES['application_files__cv']['name'],
$time);
// move application_files__certificates
if (count($_FILES['application_files__certificates']['name'])) {
foreach ($_FILES['application_files__certificates']['name'] as $i => $name) {
$moveFile($_FILES['application_files__certificates']['tmp_name'][$i],
$_FILES['application_files__certificates']['name'][$i],
$time);
$a_list[] = 'assets/siteContents/bewerbungen/'.$time.'_'.preg_replace('/[^a-zA-Z0-9\.]/s', '_',$_FILES['application_files__certificates']['name'][$i]);
}
}
If the error isn't caused by the special chars in the filename I am not sure what the problem might be.
The function returns FALSE on this conditions:
If filename is not a valid upload file, then no action will occur, and
move_uploaded_file() will return FALSE.
If filename is a valid upload file, but cannot be moved for some
reason, no action will occur, and move_uploaded_file() will return
FALSE. Additionally, a warning will be issued.
(Source: PHP.NET)
You are filtering your filename with this regex /[^a-zA-Z0-9\.]/s but this will only replace the first occurence of a special char not all.
Try adding the "g" modifier: /[^a-zA-Z0-9\.]/sg
Are you sure your client has the right permissions to move the file
to the desired location? I would double check them.

What is the security issue with my code?

A few years ago, I posted an answer to a question about a way, in PHP, to let the user pass in the URI the relative path to the file to download, while preventing directory traversal.
I got a few comments telling that the code is insecure, and a few downvotes (the most recent being today). Here's the code:
$path = $_GET['path'];
if (strpos($path, '../') !== false ||
strpos($path, "..\\") !== false ||
strpos($path, '/..') !== false ||
strpos($path, '\..') !== false)
{
// Strange things happening.
}
else
{
// The request is probably safe.
if (file_exists(dirname(__FILE__) . DIRECTORY_SEPARATOR . $path))
{
// Send the file.
}
else
{
// Handle the case where the file doesn't exist.
}
}
I reviewed the code again and again, tested it, and still can't understand what's the security issue it introduces.
The only hint I got in the comments is that ../ can be replaced by %2e%2e%2f. This is not an issue, since PHP will automatically transform it into ../.
What is the problem with this piece of code? What could be the value of the input which would allow directory traversal or break something in some way?
There are lots of other possibilities that could slip through, such as:
.htaccess
some-secret-file-with-a-password-in-it.php
In other words, anything in the directory or a subdirectory would be accessible, including .htaccess files and source code. If anything in that directory or its subdirectories should not be downloadable, then that's a security hole.
I've just ran your code through Burp intruder and cannot find any way round it in this case.
It was probably down voted due to exploits against other/old technology stacks which employed a similar approach by blacklisting certain character combinations.
As you mention, the current version of PHP automatically URL decodes input, but there have been flaws where techniques such as double URL encoding (dot = %252e), 16 bit Unicode encoding (dot = %u002e), overlong UTF-8 Unicode encoding (dot = %c0%2e) or inserting a null byte (%00) could trick the filter and allow the server side code to interpret the path as the unencoded version once it had been given a thumbs up by the filter.
This is why it has set alarm bells ringing. Even though your approach appears to work here, generally it may not be the case. Technology is always changing and it is always best to err on the side of caution and use techniques that are immune to character set interpretations wherever possible such as using whitelists of known good characters that will likely to be always good, or using a file system function (realpath was mentioned in the linked answer) to verify that the actual path is the one you're expecting.
I can’t think of any case in which this should fail.
However, I don’t know how PHP’s file_exists is implemented internally and whether it has some currently unknown quirks. Just like PHP had null byte related issues with some file system functions until PHP 5.3.4.
So to play it safe, I’d rather like to check the already resolved path instead of blindly trusting PHP and – probably more important – my assumption, the four mentioned sequences are the only ones that can result in a path that is above the designated base directory. That’s why I would prefer ircmaxell’s solution to yours.
Blacklisting is a bad habit. You're better off with a whitelist (either on the literal strings allowed or on the characters allowed.)
if(preg_match('/^[A-Za-z0-9\-\_]*$/', $path) ) {
// Yay
} else {
// No
}
Or alternatively:
switch($path) {
case 'page1':
case 'page2':
// ...
break;
default:
$path = 'page1';
break;
}
include $path;

What is the best method to determine if a value is either a base64 string or an image URL?

I have a value that may be either an image URL or an image Base64 string. What is the best method to determine which is which? If it's an image URL, the image will already reside on my server.
I've tried doing a preg_match but I think running a preg_match on a potentially huge base64 string will be server intense.
EDIT: The two best methods thus far.
// if not base64 URL
if (substr($str, 0, 5) !== 'data:') {}
// if file exists
if (file_exists($str)) {}
You mean you want to differentiate between
<img src="http://example.com/kittens.jpg" />
and
<img src="data:image/png;base64,...." />
You'd only need to look at the first 5 chars of the src attribute to figure out if it's a data uri, e.g.
if (substr($src, 0, 5) == 'data:')) {
... got a data uri ...
}
If it doesn't look like a data uri, then it's safe to assume it's a URL and treat it as such.
If those are only two possibilities, you can do something like:
$string = 'xxx';
$part = substr($string, 0, 6); //if an image, it will extract upto http(s):
if(strstr($part, ':')) {
//image
} else {
//not an image
}
Explanation: The above code assumes that the input is either a base64 string or an image. If it's an image, it will and should contain the protocol information (including the :). That's not allowed in a base64 encoded string.
You can do this with preg_match(). When preg_match doesn't see the d, the code will stop. if it finds a d not followed by an a it will stop and so on. Also this way you're not doing superfluous math and string parsing:
if(!preg_match('!^data\:!',$str) {
//image
} else {
//stream
}
You can also use is_file() which will not return true on directories.
// if file exists and is a file and not a directory
if (is_file($str)) {}

PHP Security - Sanitize & Clean

I have a built a script around class.upload from http://www.verot.net/php_class_upload.htm
Basically what it is that all my images are stored on the server in a directory called /images/
The script I built basically takes some parameters from my website such as /xyzDir/tomnjerry.jpg?w=100&h=100&fill=1&color=fff
Then I have mod_rewrite which reads the file from /xyzDir/ into a php script which then translates the width and height and returns the image.
Lately I have noticed some idiots from Turkey trying to input weird characters into the parameters w= and h=
On my script I do check to make sure only integer is allowed in width and heigh and fill can be either 1 or 2 and color can only be certain values which i check via array.
I just want to see if there is anything else I should be doing in order to avoid getting hacked.
Thanks
Always remember, Filter In, Escape Out for all user supplied (or untrusted) input.
When reading user supplied data, filter it to known values. DO NOT BLACKLIST! Always always always always whitelist what you are expecting to get. If you're expecting a hex number, validate it with a regex like: ^[a-f0-9]+$. Figure out what you expect, and filter towards that. Do none of your filenames have anything but alpha, numeric and .? Then filter to ^[a-z0-9.]+$. But don't start thinking blacklisting against things. It won't work.
When using user-data, escape it properly for the use at hand. If it's going in a database, either bind it as a parameterized query, or escape it with the database's escape function. If you're calling a shell command, escape it with escapeshellarg(). If you're using it in a regex pattern, escape it with preg_quote(). There are more than that, but you get the idea.
When outputting user data, escape it properly for the format you're outputting it as. If you're outputting it to HTML or XML, use htmlspecialchars(). If you're outputting to raw headers for some reason, escape any linebreaks (str_replace(array("\r", "\n"), array('\r', '\n'), $string)). Etc, etc, etc.
But always filter using a white-list, and always escape using the correct method for the context. Otherwise there's a significant chance you'll miss something...
create a validation class to validate your post params like so.
class MyValidation
{
public function is_interger($val)
{
return is_int($val);
}
public function within_range($val,$min,$max)
{
if($this->is_interger($val))
{
return ($val < $max && $val > $min);
}
return false;
}
public function is_hex($val)
{
return preg_match("/^([a-f0-9]{3}){1,2}$/",$val);
}
}
And use to validate your values.
Example:
$Validator = new MyValidation();
if($Validator->is_hex($_POST['color']))
{
//Sweet.
}
Make sure the image name does not contain string like "../". Depending on your script, that could be a way to step out images directory and make the script deliver other files.
You should use intval() for ensuring that the width and height are integers
$width = intval($_GET['w']);
$height = intval($_GET['h']);
You can do
$fill = $fill == 1 ? 1 : 2;
Which is a ternary operator, so if it's anything apart from 1 it's going to be set to 2.
As for validation of hex, the rules of hex dictate that it must be in range of 0-9/A-F.
$color = preg_replace('/[^0-9a-f]/i', "", $_GET['color']);
Hope that helps.
(It should be noted that my suggested code will perform the manipulation required to make it suitable for your page, rather than confirming that is is valid before hand)
No one's mentioned the filter extension here which provides great filtering natively implemented in the PHP engine. IMHO this is a great extension and should always be used before rolling your own filtering code. For example, checking for an integer is as simple as:
<?php
if (filter_var($value, FILTER_VALIDATE_INT)) {
//Valid Integer.
}
?>
Validating a hex number can be done with:
<?php
if (filter_var($value, FILTER_VALIDATE_INT, FILTER_FLAG_ALLOW_HEX)) {
//Valid Hex number
}
?>
Then I have mod_rewrite which reads the file from /xyzDir/ into a php script which then translates the width and height and returns the image.
If you include the file, image or other type, it will execute any PHP code buried within it. So if you didn't shake off any possible code appended to a user uploaded image by reformatting it through imagemagick or gd into a completely new file, that is one way your server can be compromised.
So for example if you serve the user uploaded image like this...
<?php
header('Content-type: image/jpeg');
header('Content-Disposition: attachment; filename="tomnjerry.jpg"');
include('xyzDir/tomnjerry.jpg');
?>
...and if the user opened the jpg in a raw text editor and appended <?php phpinfo(); ?> to the very end before uploading it to your server, then they can browse to and download it and extract all phpinfo details of your PHP installation from it.
But since you mentioned resizing the image first, you're probably not even serving the image this way. So you should be safe from this attack.

Categories