I am implementing a simple directory listing script in PHP.
I want to ensure that the passed path is safe before opening directory handles and echoing the results willy-nilly.
$f = $_GET["f"];
if(! $f) {
$f = "/";
}
// make sure $f is safe
$farr = explode("/",$f);
$unsafe = false;
foreach($farr as $farre) {
// protect against directory traversal
if(strpos($farre,"..") != false) {
$unsafe = true;
break;
}
if(end($farr) != $farre) {
// make sure no dots are present (except after the last slash in the file path)
if(strpos($farre,".") != false) {
$unsafe = true;
break;
}
}
}
Is this enough to make sure a path sent by the user is safe, or are there other things I should do to protected against attack?
It may be that realpath() is helpful to you.
realpath() expands all symbolic links
and resolves references to '/./',
'/../' and extra '/' characters in the
input path, and returns the
canonicalized absolute pathname.
However, this function assumes that the path in question actually exists. It will not perform canonization for a non-existing path. In this case FALSE is returned.
Related
I've created a function to call file from folder. But the problem is that it is not matching case. My function is
function CheckFile($var){
if($var){
$file = $_SERVER['DOCUMENT_ROOT'].'/include_folder/'.$var.'.php';
if (file_exists($file)) {
return true;
}
}
}
So if file is exists, I include it. Like if $var = profile then it will check in core folder for profile.php and if it exists then it will include it. I am including file when I am calling this function. But the problem is that it is not case sensitive. like if I look for "PrOFile" then it will include profile.php so how to solve this? Please help me if anyone can.
Use realpath() combined with converting the slashes if needed.
Examples:
// Check the filename is the same (don't worry on the path)
if (file_exists($filepath) && basename(realpath($filepath)) === basename($filepath)) {
// Check the full path, converting slashes to be sure
// Assumes final path is Lunix orientated
if (file_exists($filepath) && str_replace('\\', '/', realpath($filepath)) === $filepath) {
// Check the full path, converting slashes to be sure
// Assumes final path may not be Lunix orientated
if (file_exists($filepath) && str_replace('\\', '/', realpath($filepath)) === str_replace('\\', '/', $filepath)) {
On Windows, filenames are case-insensitive. That's just how they work.
So you need to manually code around this:
if( file_exists($file) && glob($file)[0] === $file) // assumes PHP 5.4
// older versions need a temporary variable for glob
I am currently displaying the file name from the database on my PHP page. However, some file names on the server's folders have a different case. So the database may say image1.jpg and the file name on the server may say "image1.JPG" in upper case. This is random with some of the files. These files do not get displayed. Is there a way that I can use a function so that it can be displayed. We are talking about more than 1000 files here. So any help would be highly appreciated.
I would run a custom file_exists() function to check for which case the image's extension is.
Use this custom function to check for the correct case (pass it lowercase, then use lowercase if it returns a 1, or use uppercase if it returns a 2):
function file_exists_case($strUrl)
{
$realPath = str_replace('\\','/',realpath($strUrl));
if(file_exists($strUrl) && $realPath == $strUrl)
{
return 1; //File exists, with correct case
}
elseif(file_exists($realPath))
{
return 2; //File exists, but wrong case
}
else
{
return 0; //File does not exist
}
}
You really should go in and make all your file name extensions lowercase when you get the time, though.
The way you would do that is by running a glob() through the directories: http://php.net/manual/en/function.glob.php and renaming every file extension to lowercase using strtolower(): http://php.net/manual/en/function.strtolower.php
Not sure if converting the extensions to lowercase is an option. But if there are no other systems that depend on certain extensions to be capitalized then you could run something like this:
find . -name '*.*' -exec sh -c '
a=$(echo {} | sed -r "s/([^.]*)\$/\L\1/");
[ "$a" != "{}" ] && mv "{}" "$a" ' \;
Use file_exists to do a check. And expand that out to compensate for the issues you are facing. I am using the function called replace_extension() shown here.
<?php
// Full path to the file.
$file_path = '/path/to/the/great.JPG';
// Call to the function.
echo check_if_image_exists($file_path, $file_ext_src);
// The function itself.
function check_if_image_exists ($file_path) {
$file_ext_src = end(explode('.', $file_path));
if (file_exists($file_path)) {
return TRUE;
}
else {
if (ctype_lower($file_ext_src)) {
$file_ext_new = strtoupper($file_ext_src); // If lowercase make it uppercase.
}
else if (ctype_upper($file_ext_src)) {
$file_ext_new = strtolower($file_ext_src); // If uppercase make it lowercase.
}
// Now create a new filepath with the new extension & check that.
$file_path_new = replace_extension($file_path, $file_ext_new);
if (file_exists($file_path_new)) {
return TRUE;
}
else {
return FALSE;
}
}
}
// Nice function taken from elsewhere.
function replace_extension($filename, $new_extension) {
$info = pathinfo($filename);
return $info['filename'] . '.' . $new_extension;
}
?>
I have a function to check if a file exists via jQuery which makes a call to a PHP script which I'll use when changing certain images at the click of a button on my index page.
jQuery function:
function fileExists(path){
$.getJSON("/ajax/fileExists.php",{ path: path },
function (data){
return data.path;
});
}
fileExists.php:
$path=$_SERVER['DOCUMENT_ROOT'].'/packs'.$_GET['path'];
if(file_exists($path)){
echo json_encode(TRUE);
}else{
echo json_encode(FALSE);
}
I'm worried about people using this script to list the contents of my server or files which I may not want them to know about so I've used DOCUMENT_ROOT and /packs to try to limit calls to that directory but I think people can simply use ../ within the supplied path to check alternatives.
What is the best way to make this safe, ideally limit it to /packs, and are there any other concerns I should worry about?
Edit: an example call in javascript/jQuery:
if( fileExists('/index.php') ){
alert('Exists');
}else{
alert('Doesn\'t exist');
}
This is how I've handled it in the past:
$path = realpath($_SERVER['DOCUMENT_ROOT'].'/packs'.$_GET['path']);
if (strpos($path, $_SERVER['DOCUMENT_ROOT']) !== 0) {
//It's looking to a path that is outside the document root
}
You can remove any path-transversing from your filename:
$path_arr = explode("/", $_GET['path']);
$path = $path_arr[count($path_arr - 1)];
Such a practice is moderately secure and fast (O(1) complexity) but is not really the best as you have to watch out for encoding, character replacement and all like stuff.
But the overall best practice (though less faster depending on your directory size, let's say O(n) complexity) would be to use readdir() to get a list of all the files in your /packs directory then see if the supplied filename is present:
$handle = opendir($path=$_SERVER['DOCUMENT_ROOT'].'/packs');
while (false !== ($entry = readdir($handle))) {
if ($entry === $_GET['path']) {
echo json_encode(TRUE);
return;
}
}
echo json_encode(FALSE);
I am working on bookmarklet and I am fetching all the photos of any external page using HTML DOM parser(As suggested earlier by SO answer). I am fetching the photos correctly and displaying that in my bookmarklet pop up. But I am having problem with the relative path of photos.
for example the photo source on external page say http://www.example.com/dir/index.php
photo Source 1 : img source='hostname/photos/photo.jpg' - Getting photo as it is absolute
photo Source 2 : img source='/photos/photo.jpg' - not getting as it is not absolute.
I worked through the current url I mean using dirname or pathinfo for getting directory by current url. but causes problem between host/dir/ (gives host as parent directory ) and host/dir/index.php (host/dir as parent directory which is correct)
Please help How can I get these relative photos ??
FIXED (added support for query-string only image paths)
function make_absolute_path ($baseUrl, $relativePath) {
// Parse URLs, return FALSE on failure
if ((!$baseParts = parse_url($baseUrl)) || (!$pathParts = parse_url($relativePath))) {
return FALSE;
}
// Work-around for pre- 5.4.7 bug in parse_url() for relative protocols
if (empty($baseParts['host']) && !empty($baseParts['path']) && substr($baseParts['path'], 0, 2) === '//') {
$parts = explode('/', ltrim($baseParts['path'], '/'));
$baseParts['host'] = array_shift($parts);
$baseParts['path'] = '/'.implode('/', $parts);
}
if (empty($pathParts['host']) && !empty($pathParts['path']) && substr($pathParts['path'], 0, 2) === '//') {
$parts = explode('/', ltrim($pathParts['path'], '/'));
$pathParts['host'] = array_shift($parts);
$pathParts['path'] = '/'.implode('/', $parts);
}
// Relative path has a host component, just return it
if (!empty($pathParts['host'])) {
return $relativePath;
}
// Normalise base URL (fill in missing info)
// If base URL doesn't have a host component return error
if (empty($baseParts['host'])) {
return FALSE;
}
if (empty($baseParts['path'])) {
$baseParts['path'] = '/';
}
if (empty($baseParts['scheme'])) {
$baseParts['scheme'] = 'http';
}
// Start constructing return value
$result = $baseParts['scheme'].'://';
// Add username/password if any
if (!empty($baseParts['user'])) {
$result .= $baseParts['user'];
if (!empty($baseParts['pass'])) {
$result .= ":{$baseParts['pass']}";
}
$result .= '#';
}
// Add host/port
$result .= !empty($baseParts['port']) ? "{$baseParts['host']}:{$baseParts['port']}" : $baseParts['host'];
// Inspect relative path path
if ($relativePath[0] === '/') {
// Leading / means from root
$result .= $relativePath;
} else if ($relativePath[0] === '?') {
// Leading ? means query the existing URL
$result .= $baseParts['path'].$relativePath;
} else {
// Get the current working directory
$resultPath = rtrim(substr($baseParts['path'], -1) === '/' ? trim($baseParts['path']) : str_replace('\\', '/', dirname(trim($baseParts['path']))), '/');
// Split the image path into components and loop them
foreach (explode('/', $relativePath) as $pathComponent) {
switch ($pathComponent) {
case '': case '.':
// a single dot means "this directory" and can be skipped
// an empty space is a mistake on somebodies part, and can also be skipped
break;
case '..':
// a double dot means "up a directory"
$resultPath = rtrim(str_replace('\\', '/', dirname($resultPath)), '/');
break;
default:
// anything else can be added to the path
$resultPath .= "/$pathComponent";
break;
}
}
// Add path to result
$result .= $resultPath;
}
return $result;
}
Tests:
echo make_absolute_path('http://www.example.com/dir/index.php','/photos/photo.jpg')."\n";
// Outputs: http://www.example.com/photos/photo.jpg
echo make_absolute_path('http://www.example.com/dir/index.php','photos/photo.jpg')."\n";
// Outputs: http://www.example.com/dir/photos/photo.jpg
echo make_absolute_path('http://www.example.com/dir/index.php','./photos/photo.jpg')."\n";
// Outputs: http://www.example.com/dir/photos/photo.jpg
echo make_absolute_path('http://www.example.com/dir/index.php','../photos/photo.jpg')."\n";
// Outputs: http://www.example.com/photos/photo.jpg
echo make_absolute_path('http://www.example.com/dir/index.php','http://www.yyy.com/photos/photo.jpg')."\n";
// Outputs: http://www.yyy.com/photos/photo.jpg
echo make_absolute_path('http://www.example.com/dir/index.php','?query=something')."\n";
// Outputs: http://www.example.com/dir/index.php?query=something
I think that should deal with just about everything your likely to encounter correctly, and should equate to roughly the logic used by a browser. Also should correct any oddities you might get on Windows with stray forward slashes from using dirname().
First argument is the full URL of the page where you found the <img> (or <a> or whatever) and second argument is the contents of the src/href etc attribute.
If anyone finds something that doesn't work (cos I know you'll all be trying to break it :-D), let me know and I'll try and fix it.
'/' should be the base path. Check the first character returned from your dom parser, and if it is a '/' then just prefix it with the domain name.
After the user uploads an image to the server, should we sanitize $_FILES['filename']['name']?
I do check file size/file type etc. But I don't check other things. Is there a potential security hole?
Thank you
Absolutely! As #Bob has already mentioned it's too easy for common file names to be overwritten.
There are also some issues that you might want to cover, for instance not all the allowed chars in Windows are allowed in *nix, and vice versa. A filename may also contain a relative path and could potentially overwrite other non-uploaded files.
Here is the Upload() method I wrote for the phunction PHP framework:
function Upload($source, $destination, $chmod = null)
{
$result = array();
$destination = self::Path($destination);
if ((is_dir($destination) === true) && (array_key_exists($source, $_FILES) === true))
{
if (count($_FILES[$source], COUNT_RECURSIVE) == 5)
{
foreach ($_FILES[$source] as $key => $value)
{
$_FILES[$source][$key] = array($value);
}
}
foreach (array_map('basename', $_FILES[$source]['name']) as $key => $value)
{
$result[$value] = false;
if ($_FILES[$source]['error'][$key] == UPLOAD_ERR_OK)
{
$file = ph()->Text->Slug($value, '_', '.');
if (file_exists($destination . $file) === true)
{
$file = substr_replace($file, '_' . md5_file($_FILES[$source]['tmp_name'][$key]), strrpos($value, '.'), 0);
}
if (move_uploaded_file($_FILES[$source]['tmp_name'][$key], $destination . $file) === true)
{
if (self::Chmod($destination . $file, $chmod) === true)
{
$result[$value] = $destination . $file;
}
}
}
}
}
return $result;
}
The important parts are:
array_map('basename', ...), this makes sure that the file doesn't contain any relative paths.
ph()->Text->Slug(), this makes sure only .0-9a-zA-Z are allowed in the filename, all the other chars are replaced by underscores (_)
md5_file(), this is added to the filename iff another file with the same name already exists
I prefer to use the user supplied name since search engines can use that to deliver better results, but if that is not important to you a simple microtime(true) or md5_file() could simplify things a bit.
Hope this helps! =)
The filename is an arbitrary user supplied string. As a general rule, never trust arbitrary user supplied values.
You should never use the user supplied filename as the name to save the file under on the server, always create your own filename. The only thing you may want to do with it is to save it as metadata for informational purposes. When outputting that metadata, take the usual precautions like sanitation and escaping.
you also need to check for duplicate names. It's too easy for multiple people to upload an image called 'mycat.jpg', which if uploaded to the same folder would overwrite a previously uploaded file by the same name. You can do this by putting a unique id in the file name (as Prix suggests). Also verify that the file type doesn't just end with an image extension but also is an actual image; you don't want your server acting as a blind host for random files.