Absolute path without symlink resolution / Keep user in home directory
Asked Answered
E

4

10

Is there a way in PHP how to determine an absolute path given a relative path without actually resolving symlinks? Something like the realpath function but without symlink resolution.

Or alternatively, is there some easy way to check whether user (who uses my browsing PHP script to view files) did not accidentally step out of Apache virtual host's home directory? (or disallow him to use the nasty . and .. in paths)

Thanks!

Environmentalist answered 29/6, 2011 at 10:15 Comment(0)
O
2

I don't know PHP native solution for this, but here's one nice absolute path implementation from a comment in the PHP docs:

Because realpath() does not work on files that do not exist, I wrote a function that does. It replaces (consecutive) occurences of / and \ with whatever is in DIRECTORY_SEPARATOR, and processes /. and /.. fine. Paths returned by get_absolute_path() contain no (back)slash at position 0 (beginning of the string) or position -1 (ending).

function get_absolute_path($path) {
   $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;
       }
   }
   return implode(DIRECTORY_SEPARATOR, $absolutes);
}

A test:

var_dump(get_absolute_path('this/is/../a/./test/.///is'));

Returns:

string(14) "this/a/test/is"

As you can see, it also produces Yoda-speak. :)

(edited for formatting & typos)

Ostrich answered 29/6, 2011 at 10:47 Comment(2)
How does this avoid symlink resolution?Esdraelon
@MikePretzlaw Unlike realpath, this function does not do anything on file system level, because it works on string modification only. And this is the whole point! :-)Ostrich
H
0

You can use open_basedir setting ini php.ini or in virtual host declaration (as php_admin_value directive).

Homiletics answered 29/6, 2011 at 10:27 Comment(0)
M
0

Here is my approach mimicking the original realpath() which also:

  1. Adds / removes Windows drive letters as in c:.
  2. Removes trailing slashes.
  3. Prepends current working directory to relative paths.
  4. Optionally checks file existence. In case of an non existing file A) do nothing, B) return a FALSE value or C) throw an error.
  5. Optionally follows symbolic links.

NB: This approach uses regular expressions which are known to be CPU expensive, so I like the method using arrays from http://www.php.net/manual/en/function.realpath.php#84012.

// Set constants for when a file does not exist.
// 0: Check file existence, set FALSE when file not exists.
define('FILE_EXISTENCE_CHECK', 0);
// 1: Require file existence, throw error when file not exists.
define('FILE_EXISTENCE_CHECK_REQUIRE_FILE', 1);
// 2: Do not check file existence.
define('FILE_EXISTENCE_CHECK_SKIP', 2);
// Windows flag.
define('IS_WINDOWS', preg_match('#WIN(DOWS|\d+|_?NT)#i', PHP_OS));
// Directory separator shortcuts.
define('DIR_SEP', DIRECTORY_SEPARATOR);
define('PREG_DIR_SEP', preg_quote(DIR_SEP));

/**
 * The original realpath() follows symbolic links which makes it harder to check
 * their paths.
 *
 * Options
 *   file_existence_check:
 *   - FILE_EXISTENCE_CHECK_REQUIRE_FILE: Script will break if the checked
 *     file does not exist (default).
 *   - FILE_EXISTENCE_CHECK: If a file does not exist, a FALSE value will be
 *     returned.
 *   - FILE_EXISTENCE_CHECK_SKIP: File existence will not be checked at all.
 *
 *   follow_link: Resolve a symbolic link or not (default: FALSE).
 */
function _realpath($path = NULL, $options = array()) {
  // Merge default options with user options.
  $options = array_merge(array(
    'file_existence_check' => FILE_EXISTENCE_CHECK_REQUIRE_FILE,
    'follow_link' => FALSE,
  ), $options);

  // Use current working directory if path has not been defined.
  $path = $path ? $path : getcwd();
  // Replace slashes with OS specific slashes.
  $path = preg_replace('#[\\\/]#', DIR_SEP, $path);

  // Handle `./`. Another great approach using arrays can be found at:
  // @link p://php.net/manual/en/function.realpath.php#84012
  $path = preg_replace('#' . PREG_DIR_SEP . '(\.?' . PREG_DIR_SEP . ')+#', DIR_SEP, $path);
  // Handle `../`.
  while (preg_match('#^(.*?)' . PREG_DIR_SEP . '[^' . PREG_DIR_SEP . ']+' . PREG_DIR_SEP . '\.\.($|' . PREG_DIR_SEP . '.*)#', $path, $m)) {
    $path = $m[1] . $m[2];
  }
  // Remove trailing slash.
  $path = rtrim($path, DIR_SEP);

  // If we are on Windows.
  if (IS_WINDOWS) {
    // If path starts with a lowercase drive letter.
    if (preg_match('#^([a-z]:)(.*)#', $path, $m)) {
      $path = strtoupper($m[1]) . $m[2];
    }
    // If path starts with a slash instead of a drive letter.
    elseif ($path[0] === DIR_SEP) {
      // Add current working directory's drive letter, ie. "D:".
      $path = substr(getcwd(), 0, 2) . $path;
    }
  }
  else {
    // Remove drive letter.
    if (preg_match('#^[A-Z]:(' . PREG_DIR_SEP . '.*)#i', $path, $m)) {
      $path = $m[1];
    }
  }

  // If path is relative.
  if (!preg_match('#^([A-Z]:)?' . PREG_DIR_SEP . '#', $path)) {
    // Add current working directory to path.
    $path = getcwd() . DIR_SEP . $path;
  }

  // If file existence has to be checked and file does not exist.
  if ($options['file_existence_check'] !== DSC_FILE_EXISTENCE_CHECK_SKIP && !file_exists($path)) {
    // Return FALSE value.
    if ($options['file_existence_check'] === DSC_FILE_EXISTENCE_CHECK) {
      return FALSE;
    }
    // Or throw error.
    else {
      dsc_print_error('File does not exist: ' . $path);
    }
  }

  // Follow sybmolic links, but only if the file exists.
  if (!empty($options['follow_link']) && file_exists($path)) {
    $path = readlink($path);
  }

  return $path;
}
Militarist answered 10/5, 2015 at 9:10 Comment(0)
L
0

@Karolis provided the accepted answer, but that does not quite do what I read the question to be. For example, the following script:

<?php
include('Karolis-answer') ;
chdir('/home/username') ;
printf("%s\n", get_absolute_path('some/folder/below') ;

when executed, will produce:

some/folder/below

... which is not an absolute path. In his defense, Karolis alluded to this in his answer, saying "... contain no (back)slash at position 0"

The correct answer would be:

/home/username/some/folder/below

The following slight modification to @Karolis' answer would be:

function absolutePath(string $path, $startingFolder = null) : string {
        // Credit to Karolis at https://mcmap.net/q/1110942/-absolute-path-without-symlink-resolution-keep-user-in-home-directory
        if (is_null($startingFolder))
            $startingFolder = getcwd() ;
        $path = str_replace( [ '/', '\\' ], DIRECTORY_SEPARATOR, $path) ;
        if (substr($path, 0, 1) != DIRECTORY_SEPARATOR)
            $path = $startingFolder . DIRECTORY_SEPARATOR . $path ;
            $parts = array_filter(explode(DIRECTORY_SEPARATOR, $path), 'strlen') ;
        $absolutes = [] ;
        foreach ($parts as $part) {
            if ( $part == '.' )
                continue ;
            if ( $part == '..')
                array_pop($absolutes) ;
            else
                $absolutes[] = $part ;
            }
        return DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $absolutes) ;
        }

This still does not resolve symbolic links, but will resolve the "relative" portion of the passed value.

Loam answered 30/6, 2023 at 14:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.