I am having php admin panel and pure-ftpd server running in the background.
I want to check if the path that user has provided for specific user DOES NOT contain ../ /.. or /../
I am using a php framework Nette and for this validation I am using addRule(Form::PATTERN, 'error message', $pattern) (Nette api-docs: https://api.nette.org/2.4/Nette.Forms.Form.html, Nette docs: https://doc.nette.org/en/2.4/form-validation
Thank you for your time
I don't know if you're open to other solutions, but if it were me I would not attempt to use regex on the path because PHP has an easier way.
If it were me, I would do this:
<?php
// You know your users are limited to their own home directory
$user_base_dir = '/home/skunkbad/';
// And they supply some path that attempts directory transversal
$supplied_path = '/home/skunkbad/../../etc/';
// Realpath in this case would return "/etc/"
$realpath = realpath( $supplied_path );
// So the check to see if the user base directory is in the path that realpath returns would fail
if( stripos( $realpath, $user_base_dir ) !== 0 )
echo 'Invalid Path!';
See: http://php.net/manual/en/function.realpath.php
and: http://php.net/manual/en/function.stripos.php
Thank you #BrianGottier,
but realpath($path) checks if the file really exists in the filesystem and script need permission to execute... So, since I knew what I was looking for I googled and with some stuff from php.net & community i have came up with this:
/**
* #param $path
* #param $baseDir
* #return bool
*/
protected function isDirValid($path, $baseDir)
{
if (stripos($path, '\\'))
return false;
$path = str_replace('/', '/', $path);
$path = str_replace('\\', '/', $path);
$parts = array_filter(explode('/', $path), 'strlen');
$absolutes = [];
foreach ($parts as $part)
{
if ('.' == $part)
continue;
if ('..' == $part)
{
array_pop($absolutes);
} else {
$absolutes[] = $part;
}
}
$realDir = '/' . implode('/', $absolutes);
if (stripos($realDir, $baseDir) !== 0)
return false;
return true;
}
Related
I have ftp server with a lot of subfolders and files in it. I need to retrieve the folder structure from ftp, which shows names of all folders and subfolders from a specified starting path. I'm not interested in files included in each folder, only the directory tree. I'm using PHP and my server does not support mlsd.
Thanks for help.
I implemented my own recursive function, which for some reason is not working.
function ftp_list_files_recursive($ftp_stream, $path) {
$lines = ftp_rawlist($ftp_stream, $path);
$result = [];
if (is_array($lines) || is_object($lines)) {
foreach ($lines as $line) {
$exp0 = explode('<', $line);
if (sizeof($exp0) > 1):
$exp1 = explode('>', $exp0[1]);
if ($exp1[0] == 'DIR') {
$file_path=$path . "/" . ltrim($exp1[1]);
$result = array_merge($result, ftp_list_files_recursive($ftp_stream, $file_path));
} else {
$result[] = $file_path;
}
endif;
}
}
return $result;
}
The ftp_rawlist returns directory info as: 01-18-20 01:00PM <DIR> DirName so first I explode on < and check whether it was successful. If yes, then it means a string had DIR in it and it can be further exploded on >. It could have been done with regular expression, but that works for me now. If I print $file_path variable I see that it changes, so I assume the recursion works. However, the $result array is always empty. Any thoughts on that?
Start here: PHP FTP recursive directory listing.
You just need to adjust the code to:
the DOS-style listing you have from your FTP server (IIS probably) and
to collect only the folders.
function ftp_list_dirs_recursive($ftp_stream, $directory)
{
$result = [];
$lines = ftp_rawlist($ftp_stream, $directory);
if ($lines === false)
{
die("Cannot list $directory");
}
foreach ($lines as $line)
{
// rather lame parsing as a quick example:
if (strpos($line, "<DIR>") !== false)
{
$dir_path = $directory . "/" . ltrim(substr($line, strpos($line, ">") + 1));
$subdirs = ftp_list_dirs_recursive($ftp_stream, $dir_path);
$result = array_merge($result, [$dir_path], $subdirs);
}
}
return $result;
}
I'm trying to enforce a root directory in a filesystem abstraction. The problem I'm encountering is the following:
The API lets you read and write files, not only to local but also remote storages. So there's all kinds of normalisation going on under the hood. At the moment it doesn't support relative paths, so something like this isn't possible:
$filesystem->write('path/to/some/../relative/file.txt', 'file contents');
I want to be able to securely resolve the path so the output is would be: path/to/relative/file.txt.
As is stated in a github issue which was created for this bug/enhancement (https://github.com/FrenkyNet/Flysystem/issues/36#issuecomment-30319406) , it needs to do more that just splitting up segments and removing them accordingly.
Also, since the package handles remote filesystems and non-existing files, realpath is out of the question.
So, how should one go about when dealing with these paths?
To quote Jame Zawinski:
Some people, when confronted with a problem, think "I know, I'll use regular expressions."
Now they have two problems.
protected function getAbsoluteFilename($filename) {
$path = [];
foreach(explode('/', $filename) as $part) {
// ignore parts that have no value
if (empty($part) || $part === '.') continue;
if ($part !== '..') {
// cool, we found a new part
array_push($path, $part);
}
else if (count($path) > 0) {
// going back up? sure
array_pop($path);
} else {
// now, here we don't like
throw new \Exception('Climbing above the root is not permitted.');
}
}
// prepend my root directory
array_unshift($path, $this->getPath());
return join('/', $path);
}
I've resolved how to do this, this is my solution:
/**
* Normalize path
*
* #param string $path
* #param string $separator
* #return string normalized path
*/
public function normalizePath($path, $separator = '\\/')
{
// Remove any kind of funky unicode whitespace
$normalized = preg_replace('#\p{C}+|^\./#u', '', $path);
// Path remove self referring paths ("/./").
$normalized = preg_replace('#/\.(?=/)|^\./|\./$#', '', $normalized);
// Regex for resolving relative paths
$regex = '#\/*[^/\.]+/\.\.#Uu';
while (preg_match($regex, $normalized)) {
$normalized = preg_replace($regex, '', $normalized);
}
if (preg_match('#/\.{2}|\.{2}/#', $normalized)) {
throw new LogicException('Path is outside of the defined root, path: [' . $path . '], resolved: [' . $normalized . ']');
}
return trim($normalized, $separator);
}
./ current location
../ one level up
function normalize_path($str){
$N = 0;
$A =explode("/",preg_replace("/\/\.\//",'/',$str)); // remove current_location
$B=[];
for($i = sizeof($A)-1;$i>=0;--$i){
if(trim($A[$i]) ===".."){
$N++;
}else{
if($N>0){
$N--;
}
else{
$B[] = $A[$i];
}
}
}
return implode("/",array_reverse($B));
}
so:
"a/b/c/../../d" -> "a/d"
"a/./b" -> "a/b"
/**
* Remove '.' and '..' path parts and make path absolute without
* resolving symlinks.
*
* Examples:
*
* resolvePath("test/./me/../now/", false);
* => test/now
*
* resolvePath("test///.///me///../now/", true);
* => /home/example/test/now
*
* resolvePath("test/./me/../now/", "/www/example.com");
* => /www/example.com/test/now
*
* resolvePath("/test/./me/../now/", "/www/example.com");
* => /test/now
*
* #access public
* #param string $path
* #param mixed $basePath resolve paths realtively to this path. Params:
* STRING: prefix with this path;
* TRUE: use current dir;
* FALSE: keep relative (default)
* #return string resolved path
*/
function resolvePath($path, $basePath=false) {
// Make absolute path
if (substr($path, 0, 1) !== DIRECTORY_SEPARATOR) {
if ($basePath === true) {
// Get PWD first to avoid getcwd() resolving symlinks if in symlinked folder
$path=(getenv('PWD') ?: getcwd()).DIRECTORY_SEPARATOR.$path;
} elseif (strlen($basePath)) {
$path=$basePath.DIRECTORY_SEPARATOR.$path;
}
}
// Resolve '.' and '..'
$components=array();
foreach(explode(DIRECTORY_SEPARATOR, rtrim($path, DIRECTORY_SEPARATOR)) as $name) {
if ($name === '..') {
array_pop($components);
} elseif ($name !== '.' && !(count($components) && $name === '')) {
// … && !(count($components) && $name === '') - we want to keep initial '/' for abs paths
$components[]=$name;
}
}
return implode(DIRECTORY_SEPARATOR, $components);
}
How to use php keep only specific file and remove others in directory?
example:
1/1.png, 1/2.jpeg, 1/5.png ...
the file number, and file type is random like x.png or x.jpeg, but I have a string 2.jpeg the file need to keep.
any suggestion how to do this??
Thanks for reply, now I coding like below but the unlink function seems not work delete anything.. do I need change some setting? I'm using Mamp
UPDATE
// explode string <img src="u_img_p/5/x.png">
$content_p_img_arr = explode('u_img_p/', $content_p_img);
$content_p_img_arr_1 = explode('"', $content_p_img_arr[1]); // get 5/2.png">
$content_p_img_arr_2 = explode('/', $content_p_img_arr_1[0]); // get 5/2.png
print $content_p_img_arr_2[1]; // get 2.png < the file need to keep
$dir = "u_img_p/".$id;
if ($opendir = opendir($dir)){
print $dir;
while(($file = readdir($opendir))!= FALSE )
if($file!="." && $file!= ".." && $file!= $content_p_img_arr_2[1]){
unlink($file);
print "unlink";
print $file;
}
}
}
I change the code unlink path to folder, then it works!!
unlink("u_img_p/".$id.'/'.$file);
http://php.net/manual/en/function.scandir.php
This will get all files in a directory into an array, then you can run a foreach() on the array and look for patterns / matches on each file.
unlink() can be used to delete the file.
$dir = "/pathto/files/"
$exclude[] = "2.jpeg";
foreach(scandir($dir) as $file) {
if (!in_array($file, $exclude)) {
unlink("$dir/$file");
}
}
Simple and to the point. You can add multiple files to the $exclude array.
$dir = "your_folder_path";
if ($opendir = opendir($dir)){
//read directory
while(($file = readdir($opendir))!= FALSE ){
if($file!="." && $file!= ".." && $file!= "2.jpg"){
unlink($file);
}
}
}
function remove_files( $folder_path , $aexcludefiles )
{
if (is_dir($folder_path))
{
if ($dh = opendir($folder_path))
{
while (($file = readdir($dh)) !== false)
{
if( $file == '.' || $file == '..' )
continue ;
if( in_array( $file , $aexcludefiles ) )
continue ;
$file_path = $folder_path."/".$file ;
if( is_link( $file_path ) )
continue ;
unlink( $file_path ) ;
}
closedir($dh);
}
}
}
$aexcludefiles = array( "2.jpeg" )
remove_files( "1" , $aexcludefiles ) ;
I'm surprised people don't use glob() more. Here is another idea:
$dir = '/absolute/path/to/u_img_p/5/';
$exclude[] = $dir . 'u_img_p/5/2.jpg';
$filesToDelete = array_diff(glob($dir . '*.jpg'), $exclude);
array_map('unlink', $filesToDelete);
First, glob() returns an array of files based on the pattern provided to it. Next, array_diff() finds all the elements in the first array that aren't in the second. Finally, use array_map() with unlink() to delete all but the excluded file(s). Be sure to use absolute paths*.
You could even make it into a helper function. Here's a start:
<?php
/**
* #param string $path
* #param string $pattern
* #param array $exclude
* #return bool
*/
function deleteFiles($path, $pattern, $exclude = [])
{
$basePath = '/absolute/path/to/your/webroot/or/images/or/whatever/';
$path = $basePath . trim($path, '/');
if (is_dir($path)) {
array_map(
'unlink',
array_diff(glob($path . '/' . $pattern, $exclude)
);
return true;
}
return false;
}
unlink() won't work unless the array of paths returned by glob() happen to be relative to where unlink() is called. Since glob() will return only what it matches, it's best to use the absolute path of the directory in which your files to delete/exclude are contained.See the docs and comments on how glob() matches and give it a play to see how it works.
I have a python script I wrote that I need to port to php. It recursively searches a given directory and builds a string based on regex searches. The first function I am trying to port is below. It takes a regex and a base dir, recursively searches all files in that dir for the regex, and builds a list of the string matches.
def grep(regex, base_dir):
matches = list()
for path, dirs, files in os.walk(base_dir):
for filename in files:
fullpath = os.path.join(path, filename)
with open(fullpath, 'r') as f:
content = f.read()
matches = matches + re.findall(regex, content)
return matches
I never use PHP except for basic GET param manipulation. I grabbed some directory walking code from the web, and am struggling to make it work like the python function above due to my utter lack of the php API.
function findFiles($dir = '.', $pattern = '/./'){
$prefix = $dir . '/';
$dir = dir($dir);
while (false !== ($file = $dir->read())){
if ($file === '.' || $file === '..') continue;
$file = $prefix . $file;
if (is_dir($file)) findFiles($file, $pattern);
if (preg_match($pattern, $file)){
echo $file . "\n";
}
}
}
Here is my solution:
<?php
class FileGrep {
private $dirs; // Scanned directories list
private $files; // Found files list
private $matches; // Matches list
function __construct() {
$this->dirs = array();
$this->files = array();
$this->matches = array();
}
function findFiles($path, $recursive = TRUE) {
$this->dirs[] = realpath($path);
foreach (scandir($path) as $file) {
if (($file != '.') && ($file != '..')) {
$fullname = realpath("{$path}/{$file}");
if (is_dir($fullname) && !is_link($fullname) && $recursive) {
if (!in_array($fullname, $this->dirs)) {
$this->findFiles($fullname, $recursive);
}
} else if (is_file($fullname)){
$this->files[] = $fullname;
}
}
}
return($this->files);
}
function searchFiles($pattern) {
$this->matches = array();
foreach ($this->files as $file) {
if ($contents = file_get_contents($file)) {
if (preg_match($pattern, $contents, $matches) > 0) {
//echo $file."\n";
$this->matches = array_merge($this->matches, $matches);
}
}
}
return($this->matches);
}
}
// Usage example:
$fg = new FileGrep();
$files = $fg->findFiles('.'); // List all the files in current directory and its subdirectories
$matches = $fg->searchFiles('/open/'); // Search for the "open" string in all those files
?>
<html>
<body>
<pre><?php print_r($matches) ?></pre>
</body>
</html>
Be aware that:
It reads each file to search for the pattern, so it may require a lot of memory (check the "memory_limit" configuration in your PHP.INI file).
It does'nt work with unicode files. If you are working with unicode files you should use the "mb_ereg_match" function rather than the "preg_match" function.
It does'nt follow symbolic links
In conclusion, even if it's not the most efficient solution at all, it should work.
Apparently, realpath is very buggy. In PHP 5.3.1, it causes random crashes.
In 5.3.0 and less, realpath randomly fails and returns false (for the same string of course), plus it always fails on realpath-ing the same string twice/more (and of course, it works the first time).
Also, it is so buggy in earlier PHP versions, that it is completely unusable. Well...it already is, since it's not consistent.
Anyhow, what options do I have? Maybe rewrite it by myself? Is this advisable?
Thanks to Sven Arduwie's code (pointed out by Pekka) and some modification, I've built a (hopefully) better implementation:
/**
* This function is to replace PHP's extremely buggy realpath().
* #param string The original path, can be relative etc.
* #return string The resolved path, it might not exist.
*/
function truepath($path){
// whether $path is unix or not
$unipath=strlen($path)==0 || $path{0}!='/';
// attempts to detect if path is relative in which case, add cwd
if(strpos($path,':')===false && $unipath)
$path=getcwd().DIRECTORY_SEPARATOR.$path;
// resolve path parts (single dot, double dot and double delimiters)
$path = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $path);
$parts = array_filter(explode(DIRECTORY_SEPARATOR, $path), 'strlen');
$absolutes = array();
foreach ($parts as $part) {
if ('.' == $part) continue;
if ('..' == $part) {
array_pop($absolutes);
} else {
$absolutes[] = $part;
}
}
$path=implode(DIRECTORY_SEPARATOR, $absolutes);
// resolve any symlinks
if(file_exists($path) && linkinfo($path)>0)$path=readlink($path);
// put initial separator that could have been lost
$path=!$unipath ? '/'.$path : $path;
return $path;
}
NB: Unlike PHP's realpath, this function does not return false on error; it returns a path which is as far as it could to resolving these quirks.
Note 2: Apparently some people can't read properly. Truepath() does not work on network resources including UNC and URLs. It works for the local file system only.
here is the modified code that supports UNC paths as well
static public function truepath($path)
{
// whether $path is unix or not
$unipath = strlen($path)==0 || $path{0}!='/';
$unc = substr($path,0,2)=='\\\\'?true:false;
// attempts to detect if path is relative in which case, add cwd
if(strpos($path,':') === false && $unipath && !$unc){
$path=getcwd().DIRECTORY_SEPARATOR.$path;
if($path{0}=='/'){
$unipath = false;
}
}
// resolve path parts (single dot, double dot and double delimiters)
$path = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $path);
$parts = array_filter(explode(DIRECTORY_SEPARATOR, $path), 'strlen');
$absolutes = array();
foreach ($parts as $part) {
if ('.' == $part){
continue;
}
if ('..' == $part) {
array_pop($absolutes);
} else {
$absolutes[] = $part;
}
}
$path = implode(DIRECTORY_SEPARATOR, $absolutes);
// resolve any symlinks
if( function_exists('readlink') && file_exists($path) && linkinfo($path)>0 ){
$path = readlink($path);
}
// put initial separator that could have been lost
$path = !$unipath ? '/'.$path : $path;
$path = $unc ? '\\\\'.$path : $path;
return $path;
}
I know this is an old thread, but it is really helpful.
I meet a weird Phar::interceptFileFuncs issue when I implemented relative path in phpctags, the realpath() is really really buggy inside phar.
Thanks this thread give me some lights, here comes with my implementation based on christian's implemenation from this thread and this comments.
Hope it works for you.
function relativePath($from, $to)
{
$fromPath = absolutePath($from);
$toPath = absolutePath($to);
$fromPathParts = explode(DIRECTORY_SEPARATOR, rtrim($fromPath, DIRECTORY_SEPARATOR));
$toPathParts = explode(DIRECTORY_SEPARATOR, rtrim($toPath, DIRECTORY_SEPARATOR));
while(count($fromPathParts) && count($toPathParts) && ($fromPathParts[0] == $toPathParts[0]))
{
array_shift($fromPathParts);
array_shift($toPathParts);
}
return str_pad("", count($fromPathParts)*3, '..'.DIRECTORY_SEPARATOR).implode(DIRECTORY_SEPARATOR, $toPathParts);
}
function absolutePath($path)
{
$isEmptyPath = (strlen($path) == 0);
$isRelativePath = ($path{0} != '/');
$isWindowsPath = !(strpos($path, ':') === false);
if (($isEmptyPath || $isRelativePath) && !$isWindowsPath)
$path= getcwd().DIRECTORY_SEPARATOR.$path;
// resolve path parts (single dot, double dot and double delimiters)
$path = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $path);
$pathParts = array_filter(explode(DIRECTORY_SEPARATOR, $path), 'strlen');
$absolutePathParts = array();
foreach ($pathParts as $part) {
if ($part == '.')
continue;
if ($part == '..') {
array_pop($absolutePathParts);
} else {
$absolutePathParts[] = $part;
}
}
$path = implode(DIRECTORY_SEPARATOR, $absolutePathParts);
// resolve any symlinks
if (file_exists($path) && linkinfo($path)>0)
$path = readlink($path);
// put initial separator that could have been lost
$path= (!$isWindowsPath ? '/'.$path : $path);
return $path;
}
For those Zend users out there, THIS answer may help you, as it did me:
$path = APPLICATION_PATH . "/../directory";
$realpath = new Zend_Filter_RealPath(new Zend_Config(array('exists' => false)));
$realpath = $realpath->filter($path);
I have never heard of such massive problems with realpath() (I always thought that it just interfaces some underlying OS functionality - would be interested in some links), but the User Contributed Notes to the manual page have a number of alternative implementations. Here is one that looks okay.
Of course, it's not guaranteed these implementations take care of all cross-platform quirks and issues, so you'd have to do thorough testing to see whether it suits your needs.
As far as I can see though, none of them returns a canonicalized path, they only resolve relative paths. If you need that, I'm not sure whether you can get around realpath() (except perhaps executing a (system-dependent) console command that gives you the full path.)
On Windows 7, the code works fine. On Linux, there is a problem in that the path generated starts with (in my case) home/xxx when it should start with /home/xxx ... ie the initial /, indicating the root folder, is missing.
The problem is not so much with this function, but with what getcwd returns in Linux.