【问题标题】:Sanitize file path in PHP without realpath()在没有 realpath() 的情况下清理 PHP 中的文件路径
【发布时间】:2014-02-20 16:40:10
【问题描述】:

有没有办法在不使用realpath() 的情况下安全地清理路径输入?

目的是防止像../../../../../path/to/file这样的恶意输入

 $handle = fopen($path . '/' . $filename, 'r');

【问题讨论】:

  • 你为什么不想使用realpath()
  • 你可以realpath()文件名,然后检查它以/path/to/allowed/files/开头
  • 注意: 运行脚本必须对层次结构中的所有目录具有可执行权限,否则realpath() 将返回FALSE。 php.net/manual/en/function.realpath.php
  • 我讨厌那些说为什么不使用realpath的人。你不知道realpath的局限性吗???如果我只需要一个相对路径呢??
  • 有一个限制,例如realpath 只能用于实际已经 现有文件。如果您需要将文件名的/../ 部分“清理”到即将创建的某个位置(因此尚不存在)。 realpath 不起作用。我也很不喜欢realpath

标签: php security sanitization


【解决方案1】:

不确定为什么您不想使用realpath,但路径名清理是一个非常简单的概念,大致如下:

  • 如果路径是相对路径(不以/开头),则在其前面加上当前工作目录和/,使其成为绝对路径。
  • 将多个/ 的所有序列替换为一个(a)
  • 将所有出现的/./ 替换为/
  • 删除/.(如果在末尾)。
  • /anything/../ 替换为/
  • 删除/anything/..(如果在末尾)。

在这种情况下,文本anything 表示不是/ 的最长字符序列。

请注意,这些规则应持续应用,直到它们都不会导致更改。换句话说,做所有六个(一次通过)。如果字符串发生变化,则返回并再次执行所有六个(另一遍)。继续这样做,直到字符串与刚刚执行的 pass 之前的字符串相同。

完成这些步骤后,您就有了一个规范的路径名,可以检查其是否存在有效模式。最有可能的是不以../ 开头的任何内容(换句话说,它不会尝试移动到起点之上。您可能还想应用其他规则,但这超出了这个问题的范围。


(a) 如果您使用的系统将路径开头的 // 视为特殊的,请确保将开头的多个 / 字符替换为 两个。这是 POSIX 唯一允许(但不强制)对多个字符进行特殊处理的地方,在所有其他情况下,多个 / 字符等同于一个字符。

【讨论】:

  • realpath 在检查时路径不存在时可能不适合(在这种情况下不适用,因为 OP 将打开文件进行读取,但为了清楚起见)
  • 注意:“将所有多于一个 / 的序列替换为一个” 不符合 RFC 3986。
  • @kontrollfreak,这是一个 URI 标准,而不是一个路径名。 POSIX/SUS 要求任意数量的连续斜杠与一个相同。 SUS 允许开头的两个斜杠具有特殊含义,但 Linux(最可能的情况)不区分。我会更新答案以澄清。
  • 如果您的脚本对整个目录层次结构没有可执行权限,realpath 也不适合。
  • path/../path2/a.txt 这样的路径呢?预计将被清理为path2/a.txt
【解决方案2】:

RFC 3986中描述了一个Remove Dot Sequence algorithm,用于在相对URI引用解析过程中,从被引用路径中解释和移除特殊的...完整路径段。

您也可以将此算法用于文件系统路径:

// as per RFC 3986
// @see https://www.rfc-editor.org/rfc/rfc3986#section-5.2.4
function remove_dot_segments($input) {
    // 1.  The input buffer is initialized with the now-appended path
    //     components and the output buffer is initialized to the empty
    //     string.
    $output = '';

    // 2.  While the input buffer is not empty, loop as follows:
    while ($input !== '') {
        // A.  If the input buffer begins with a prefix of "`../`" or "`./`",
        //     then remove that prefix from the input buffer; otherwise,
        if (
            ($prefix = substr($input, 0, 3)) == '../' ||
            ($prefix = substr($input, 0, 2)) == './'
           ) {
            $input = substr($input, strlen($prefix));
        } else

        // B.  if the input buffer begins with a prefix of "`/./`" or "`/.`",
        //     where "`.`" is a complete path segment, then replace that
        //     prefix with "`/`" in the input buffer; otherwise,
        if (
            ($prefix = substr($input, 0, 3)) == '/./' ||
            ($prefix = $input) == '/.'
           ) {
            $input = '/' . substr($input, strlen($prefix));
        } else

        // C.  if the input buffer begins with a prefix of "/../" or "/..",
        //     where "`..`" is a complete path segment, then replace that
        //     prefix with "`/`" in the input buffer and remove the last
        //     segment and its preceding "/" (if any) from the output
        //     buffer; otherwise,
        if (
            ($prefix = substr($input, 0, 4)) == '/../' ||
            ($prefix = $input) == '/..'
           ) {
            $input = '/' . substr($input, strlen($prefix));
            $output = substr($output, 0, strrpos($output, '/'));
        } else

        // D.  if the input buffer consists only of "." or "..", then remove
        //     that from the input buffer; otherwise,
        if ($input == '.' || $input == '..') {
            $input = '';
        } else

        // E.  move the first path segment in the input buffer to the end of
        //     the output buffer, including the initial "/" character (if
        //     any) and any subsequent characters up to, but not including,
        //     the next "/" character or the end of the input buffer.
        {
            $pos = strpos($input, '/');
            if ($pos === 0) $pos = strpos($input, '/', $pos+1);
            if ($pos === false) $pos = strlen($input);
            $output .= substr($input, 0, $pos);
            $input = (string) substr($input, $pos);
        }
    }

    // 3.  Finally, the output buffer is returned as the result of remove_dot_segments.
    return $output;
}

【讨论】:

  • 请注意,当给定以../ 开头的路径时,此函数(以及 Kontrolfreak 的替代实现)会默默地丢弃它,这可能不是您想要的。
【解决方案3】:

以下函数将文件系统路径和 URI 的路径组件规范化。比Gumbo's RFC implementation快。

function canonicalizePath($path)
{
    $path = explode('/', $path);
    $stack = array();
    foreach ($path as $seg) {
        if ($seg == '..') {
            // Ignore this segment, remove last segment from stack
            array_pop($stack);
            continue;
        }

        if ($seg == '.') {
            // Ignore this segment
            continue;
        }

        $stack[] = $seg;
    }

    return implode('/', $stack);
}

注意事项

  • 它不会剥离多个/ 的序列,因为这不符合RFC 3986
  • 显然,这不适用于..\backslash\paths
  • 我不确定此函数是否 100% 安全,但我无法提出会影响其输出的输入。

【讨论】:

    【解决方案4】:

    由于您只要求进行消毒,也许您需要的只是“在棘手的道路上失败”的事情。如果通常在你的路径输入中不会有任何../../stuff/../like/this,你只需要检查这个:

    function isTricky($p) {
        if(strpos("/$p/","/../")===false) return false;
        return true;
    }
    

    或者只是

    function isTricky($p) {return strpos("-/$p/","/../");}
    

    这种快速而肮脏的方式可以阻止任何向后移动,并且在大多数情况下就足够了。 (第二个版本返回一个非零而不是真,但是嘿,为什么不呢!...破折号是对字符串索引 0 的破解。)

    旁注:还要记住斜杠与反斜杠 - 我建议先将反斜杠转换为简单的斜杠。但这取决于平台。

    【讨论】:

      【解决方案5】:

      由于上述功能对我不起作用(或者已经很长),我尝试了自己的代码:

      function clean_path( $A_path="", $A_echo=false )
      {
          // IF YOU WANT TO LEAN CODE, KILL ALL "if" LINES and $A_echo in ARGS
          $_p                            = func_get_args();
          // HOW IT WORKS:
          // REMOVING EMPTY ELEMENTS AT THE END ALLOWS FOR "BUFFERS" AND HANDELLING START & END SPEC. SEQUENCES
          // BLANK ELEMENTS AT START & END MAKE SURE WE COVER SPECIALS AT BEGIN & END
          // REPLACING ":" AGAINST "://" MAKES AN EMPTY ELEMENT TO ALLOW FOR CORRECT x:/../<path> USE (which, in principle is faulty)
      
          // 1.) "normalize" TO "slashed" AND MAKE SOME SPECIALS, ALSO DUMMY ELEMENTS AT BEGIN & END 
              $_s                        = array( "\\", ":", ":./", ":../");
              $_r                        = array( "/", "://", ":/", ":/" );
              $_p['sr']                = "/" . str_replace( $_s, $_r, $_p[0] ) . "/";
              $_p['arr']                = explode('/', $_p['sr'] );
                                                                                      if ( $A_echo ) $_p['arr1']    = $_p['arr'];
          // 2.) GET KEYS OF ".." ELEMENTS, REMOVE THEM AND THE ONE BEFORE (!) AS THAT MEANS "UP" AND THAT DISABLES STEP BEFORE
              $_p['pp']                = array_keys( $_p['arr'], '..' );
              foreach($_p['pp'] as $_pos )
              {
                  $_p['arr'][ $_pos-1 ] = $_p['arr'][ $_pos ] ="";
              }
                                                                                      if ( $A_echo ) $_p['arr2']    = $_p['arr'];
          // 3.) REMOVE ALL "/./" PARTS AS THEY ARE SIMPLY OVERFLUENT
              $_p['p']                = array_keys( $_p['arr'], '.' );
              foreach($_p['p'] as $_pos )
              {
                  unset( $_p['arr'][ $_pos ] );
              }
                                                                                      if ( $A_echo ) $_p['arr3']    = $_p['arr'];
          // 4.) CLEAN OUT EMPTY ONES INCLUDING OUR DUMMIES
              $_p['arr']                = array_filter( $_p['arr'] );
          // 5) MAKE FINAL STRING
              $_p['clean']            = implode( DIRECTORY_SEPARATOR, $_p['arr'] );
                                                                                      if ($A_echo){ echo "arr=="; print_R( $_p  ); };
          return $_p['clean'];    
      }
      

      【讨论】:

        【解决方案6】:

        我更喜欢内爆/爆炸解决方案:

        public function sanitize(string $path = null, string $separator = DIRECTORY_SEPARATOR) : string
        {
            $pathArray = explode($separator, $path);
            foreach ($pathArray as $key => $value)
            {
                if ($value === '.' || $value === '..')
                {
                    $pathArray[$key] = null;
                }
            }
            return implode($separator, array_map('trim', array_filter($pathArray)));
        }
        

        以前的版本是这样的:

        public function sanitize(string $path = null, string $separator = DIRECTORY_SEPARATOR) : string
        {
            $output = str_replace(
            [
                ' ',
                '..',
            ], null, $path);
            $output = preg_replace('~' . $separator . '+~', $separator, $output);
            $output = ltrim($output, '.');
            $output = trim($output, $separator);
            return $output;
        }
        

        两者都已成功通过 this 数据提供者的测试。享受吧!

        【讨论】:

          【解决方案7】:

          简单的形式:

          $filename = str_replace('..', '', $filename);
          
          if (file_exists($path . '/' . $filename)) {
              $handle = fopen($path . '/' . $filename, 'r');
          }
          

          Le 复数形式(来自here):

          function canonicalize($address)
          {
              $address = explode('/', $address);
              $keys = array_keys($address, '..');
          
              foreach($keys AS $keypos => $key)
              {
                  array_splice($address, $key - ($keypos * 2 + 1), 2);
              }
          
              $address = implode('/', $address);
              $address = str_replace('./', '', $address);
              return $address;
          }
          echo canonicalize('/dir1/../dir2/'); // returning /dir2/
          

          【讨论】:

          • 嗯,这似乎不适用于我的 suspicious../path/name 输入 :-) 但更重要的是,它更改诸如 dir1/../dir2 之类的位置 -原版实际上是dir2,而您生成的是dir1/dir2
          • 这似乎不起作用echo canonicalize('...//dir1/dir2'); 返回../dir1/dir2
          猜你喜欢
          • 2012-03-12
          • 2019-05-17
          • 1970-01-01
          • 2012-02-07
          • 1970-01-01
          • 1970-01-01
          • 2010-11-17
          • 1970-01-01
          相关资源
          最近更新 更多