How to use $_GET path with file_exists and keep it safe? - php

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);

Related

PHP Move Files With Specific Format vs Move All Files

I have these files in /public_html/ directory :
0832.php
1481.php
2853.php
3471.php
index.php
and I want to move all those XXXX.php (always in 4 digits format) to directory /tmp/, except index.php. how to do it with reg-ex and loop?
Alternatively, how about moving all files (including index.php) first to /tmp/ then later on put only index.php back to /public_html/, which one you think is less CPU consuming?
Last thing, I found this tutorial to move file using PHP: http://www.kavoir.com/2009/04/php-copying-renaming-and-moving-a-file.html
But how to move ALL files in a directory?
You can use FilesystemIterator with RegexIterator
$source = "FULL PATH TO public_html";
$destination = "FULL PATH TO public_html/tmp";
$di = new FilesystemIterator($source, FilesystemIterator::SKIP_DOTS);
$regex = new RegexIterator($di, '/\d{4}\.php$/i');
foreach ( $regex as $file ) {
rename($file, $destination . DIRECTORY_SEPARATOR . $file->getFileName());
}
The best way would be to do it directly via the file system, but if you absolutely have to do it with PHP, something like this should do what you want - you'll have to change the paths so that they are correct, obviously. Note that this assumes that there could be other files in the public_html directory, and so it only get the filenames with 4 numbers.
$d = dir("public_html");
while (false !== ($entry = $d->read())) {
if($entry == '.' || $entry == '..') continue;
if(preg_match("#^\d{4}$#", basename($entry, ".php")) {
// move the file
rename("public_html/".$entry, "/tmp/".$entry));
}
}
$d->close();
in fact - I went to readdir manual page and the fist comment to read is:
loop through folders and sub folders with option to remove specific files.
<?php
function listFolderFiles($dir,$exclude){
$ffs = scandir($dir);
echo '<ul class="ulli">';
foreach($ffs as $ff){
if(is_array($exclude) and !in_array($ff,$exclude)){
if($ff != '.' && $ff != '..'){
if(!is_dir($dir.'/'.$ff)){
echo '<li>'.$ff.'';
} else {
echo '<li>'.$ff;
}
if(is_dir($dir.'/'.$ff)) listFolderFiles($dir.'/'.$ff,$exclude);
echo '</li>';
}
}
}
echo '</ul>';
}
listFolderFiles('.',array('index.php','edit_page.php'));
?>
Regexes are in fact overkill for this, as we only need to do some simple string matching:
$dir = 'the_directory/';
$handle = opendir($dir) or die("Problem opening the directory");
while ($filename = readdir($handle) !== false)
{
//if ($filename != 'index.php' && substr($filename, -3) == '.php')
// I originally thought you only wanted to move php files, but upon
// rereading I think it's not what you really want
// If you don't want to move non-php files, use the line above,
// otherwise the line below
if ($filename != 'index.php')
{
rename($dir . $filename, '/tmp/' . $filename);
}
}
Then for the question:
alternatively, how about moving all files (including index.php) first to /tmp/ then later on put only index.php back to /public_html/, which one you think is less CPU consuming?
It could be done, and it would probably be slightly easier on your CPU. However, there are several reasons why this doesn't matter. First off, you're already doing this in a very inefficient way by doing it through PHP, so you shouldn't really be looking at the strain this puts on your CPU at this point unless you are willing to do it outside PHP. Secondly, that would cause more disk access (especially if the source and destination directory aren't on the same disk or partition) and disk access is much, much slower than your CPU.

PHP - fastest way to find if directory has children?

I'm building a file browser, and I need to know if a directory has children (but not how many or what type).
What's the most efficient way to find if a directory has children? glob()? scandir() it? Check its tax records?
Edit
It seems I was misunderstood, although I thought I was pretty clear. I'll try to restate my question.
What is the most efficient way to know if a directory is not empty? I'm basically looking for a boolean answer - NOT EMPTY or EMPTY.
I don't need to know:
how many files are in the directory
what the files are
when they were modified
etc.
I do need to know:
does the directory have any files in it at all
efficiently.
I think this is very efficient:
function dir_contains_children($dir) {
$result = false;
if($dh = opendir($dir)) {
while(!$result && ($file = readdir($dh)) !== false) {
$result = $file !== "." && $file !== "..";
}
closedir($dh);
}
return $result;
}
It stops the listing of the directories contents as soon as there is a file or directory found (not including the . and ..).
You could use 'find' to list all empty directories in one step:
exec("find '$dir' -maxdepth 1 -empty -type d",$out,$ret);
print_r($out);
Its not "pure" php but its simple and fast.
This should do, easy, quick and effective.
<?php
function dir_is_empty($dir) {
$dirItems = count(scandir($dir));
if($dirItems > 2) return false;
else return true;
}
?>
Unfortunately, each solution so far has lacked the brevity and elegance necessary to shine above the rest.
So, I was forced to homebrew a solution myself, which I'll be implementing until something better pops up:
if(count(glob($dir."/*")) {
echo "NOT EMPTY";
}
Still not sure of the efficiency of this compared to other methods, which was the original question.
I wanted to expand vstm's answer - Check only for child directories (and not files):
/**
* Check if directory contains child directories.
*/
function dir_contains_children_dirs($dir) {
$result = false;
if($dh = opendir($dir)) {
while (!$result && ($file = readdir($dh))) {
$result = $file !== "." && $file !== ".." && is_dir($dir.'/'.$file);
}
closedir($dh);
}
return $result;
}

Should we sanitize $_FILES['filename']['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.

Optimize PHP function

I have a function that detects all files started by a string and it returns an array filled with the correspondent files, but it is starting to get slow, because I have arround 20000 files in a particular directory.
I need to optimize this function, but I just can't see how. This is the function:
function DetectPrefix ($filePath, $prefix)
{
$dh = opendir($filePath);
while (false !== ($filename = readdir($dh)))
{
$posIni = strpos( $filename, $prefix);
if ($posIni===0):
$files[] = $filename;
endif;
}
if (count($files)>0){
return $files;
} else {
return null;
}
}
What more can I do?
Thanks
http://php.net/glob
$files = glob('/file/path/prefix*');
Wikipedia breaks uploads up by the first couple letters of their filenames, so excelfile.xls would go in a directory like /uploads/e/x while textfile.txt would go in /uploads/t/e.
Not only does this reduce the number of files glob (or any other approach) has to sort through, but it avoids the maximum files in a directory issue others have mentioned.
You could use scandir() to list the files in the directory, instead of iterating through them one-by-one using readdir(). scandir() returns an array of the files.
However, it'd be better if you could change your file system organization - do you really need to store 20000+ files in a single directory?
As the other answers mention, I'd look at glob(), scandir(), and/or the DirectoryIterator class, there is no need to recreate the wheel.
However watch out! check your operating system, but there may be a limit on the maximum number of files in a single directory. If this is the case and you just keep adding files in the same directory you will have some downtime, and some problems, when you reach the limit. This error will probably appear as a permissions or write failure and not an obvious "you can't write more files in a single directory" message.
I'm not sure but probably DirectoryIterator is a bit faster. Also add caching so that list gets generated only when files are added or deleted.
You just need to compare the first length of prefix characters. So try this:
function DetectPrefix($filePath, $prefix) {
$dh = opendir($filePath);
$len = strlen($prefix);
$files = array();
while (false !== ($filename = readdir($dh))) {
if (substr($filename, 0, $len) === $prefix) {
$files[] = $filename;
}
}
if (count($files)) {
return $files;
} else {
return null;
}
}

Ensure a user-defined path is safe in PHP

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.

Categories