PHP 安全:如何防范用户上传 PHP 可执行文件

file

每一个专业的 PHP 开发者都知道用户上传的文件都是极其危险的。不管是后端和前端的黑客均可以利用它们搞事情。php

大约在一个月前,我在 reddit 上看了一篇 PHP 上传漏洞检测 ,所以, 我决定写一篇文章。用户 darpernter 问了一个棘手的问题:前端

尽管我将其重命名为 'helloworld.txt', 攻击者是否仍然可以运行他的php 脚本?

置顶的答复是:laravel

若是文件后缀修改成 .txt ,那么它不会被当作php文件执行,这样你安心了吧,不过再三确保不是 .php.txt 的后缀上传。

很差意思,问题的正确答案并不是如此 . 虽然上面的答复并不是所有错误,但显然不全面。让人惊讶的是,大多数的答案都很是类似。数据库

我想解释清楚这个问题。因此我要讨论的东西变得有点大,我决定让它变得更大。后端

问题

人们容许用户上传文件,可是担忧用户上传的文件在服务器上被执行。浏览器

从 php 文件如何被执行开始看。假设一个有 php 环境的服务器,那么它一般有两种方法在外部执行 php 文件。一是直接用 URL 请求文件,像 http://example.com/somefile.php 。第二种是 php 如今经常使用的,将全部请求转发到 index.php ,并在这个文件中以某种方式引入其余文件。因此,从 php 文件中运行代码有两种方式:执行文件或用 include/include_once/require/require_once 的方法引入其余须要运行的文件。缓存

其实还有第三种方法:eval() 函数。它能将传入的字符串当作 php 代码执行。这个函数在大多数 CMS 系统中被用来执行存储在数据库里的代码。eval() 函数很是危险,但若是你用了它,一般就意味着你确认本身在作危险的操做,并确认你已经没有其余选择。实际上, eval() 有它的用途,而且在某些状况下很是有用。但若是你是新手的话,我不推荐你使用它。请看 这篇在 OWASP 的文章。我在上面写了不少。安全

因此,有两种方法执行文件里的代码:直接执行或者在被执行的文件中引入它。那么如何避免这种事情发生呢?服务器

解决方法?

咱们怎样才能知道一个文件包含 php 代码呢?看拓展名,若是以 .php 结尾的,像 somefile.php 咱们就认为它里面有 php 代码。函数

若是在网站根目录下有一个 somefile.php 文件,那么在浏览器访问 http://example.com/somefile.php ,这个文件就会被执行而且输出内容到浏览器上。

可是若是我重命名这个文件会怎样?若是我把它重命名为 somefile.txt 或者是 somefile.jpg 呢?我会获得什么?我会获得它的内容。它不会被执行。它会从硬盘(或者缓存)直接被发送过来。

在这点上 reddit 社区上的答案是对的。重命名能防止一个文件被非预期的执行,那么为何我认为这种解决方法是错的呢?

我相信你注意到我在 “解决方法” 后面加的问号。这个问号是有意义的。如今大多数网站的 URL 上几乎看不到单独的 php 文件。而且就算有,也是人为故意伪造的,由于 URL 上须要有 .php 来实现对老版本 URL 的向后兼容。

如今绝大部分 php 代码是在运行中被引入的,由于全部请求都被发送到了网站根目录的 index.php。这个文件会根据特定的规则引入其余 php 文件。这种规则可能(或者在未来会)被恶意使用。若是你应用的规则容许引入用户的文件,那么应用会容易遭到攻击,你应该当即采起措施防止用户的文件被执行。

如何防止引入用户上传的文件?

重命名文件名能够吗? --- 不,办不到!

PHP解析器不关心文件的后缀名。事实上,全部程序都不关心。双击文件,文件会被对应的程序打开。文件后缀名只是帮助操做系统识别用什么程序打开文件。只要程序有读取文件的能力,程序就能够打开任何文件。有时程序拒绝打开和操做文件。但那并非由于后缀名,是文件内容所致。

服务器一般被设置成执行 .php  文件并将执行结果回复输出。若是你请求图片 .jpg  --- 将从磁盘上原样的返回。若是你要求服务器以某种方式运行一张 jpeg 图片,会发生?服务器会执行仍是不呢?

file

图片来源: Echo / Cultura / Getty Images

程序不关心文件名。甚至不关心文件是否有名字,也不关心它到底是不是文件。

从文件执行PHP代码须要什么?

有至少两个状况可让PHP执行代码:

  1. 代码介于 <?php 和 ?> 标记之间
  2. 代码介于 <?= 和 ?> 标记之间

即便文件中填充了一些奇怪的二进制数据或一些奇怪的保护名称,该标记中的代码仍然会被执行。

这里有一个图片给您:

file

该图片没有问题

它如今很纯净。可是您可能知道 JPEG 格式容许在文件中添加一些注释。好比,拍摄照片的相机型号或坐标地址。若是咱们试图在里面放一些PHP代码并尝试 include 或 require 呢?让咱们来看看吧!

问题! 1

下载这个图片到你的硬盘上。或者你本身去弄一张 JPEG 图片也行。你随便用什么格式的文件都无所谓。我建议用一个 JPEG 文件来演示,主要是由于它是一张图片且易于在其中进行文本编辑。我用的是一个 Windows的笔记本,目前我手头上没有 Apple 或 Linux(或其余UNIX系的系统)的笔记本。因此一会我会发一个这个 OS 下的屏幕快照。可是我确信你确定也能作这个事。

用如下这段 PHP 代码建个文件:

<h1>Problem?</h1>
<img src="troll-face.jpg">
<?php
include "./troll-face.jpg";
  1. 保存一个图片命名为troll-face.jpg
  2. 把图片和 php 脚本文件都放在同一个文件夹下
  3. 打开浏览器请求这个 php 文件

若是你把你的 php 文件命名为 index.php,而后把它放在文件根目录或者放在你网站目录下的任何一个文件目录中。

若是你准确完成了上述步骤,你就能够看到这个画面:

file

到此这都没毛病。没 PHP 代码展现,也没有 PHP 代码被执行。

如今,咱们来添加一个问题:

  1. 打开文件属性对话框或运行一些容许编辑 EXIF 信息的应用程序
  2. 切换到 Details 选项卡或以其余方式编辑该信息
  3. 向下滚动到 camera 参数
  4. 将下面代码复制到 “camera maker” 字段后面:
<?php echo "<h2>Yep, a problem!</h2>"; phpinfo(); ?>

file

刷新页面!

file

很明显出现了一点问题!

您在页面上看到了该图片。相同的图片还存在页面的 PHP 代码中。图片的代码也被执行了。

咱们该怎么作?!!1

长话短说: 若是咱们不在程序种引入这些不安全的文件,文件中的脚本就不会执行。

仔细看下面的例子。

最终答案?

若是有人在某处看到我错了 - 请纠正我,这是一个严重的问题。

PHP是一种脚本语言。您老是须要引用一些动态组合路径的文件。所以,为了保护服务器,您必须检查路径并防止混淆您的站点文件和用户上传或建立的文件。若是用户的文件与应用程序文件分开,则能够在使用上传或建立文件以前检查文件的路径。若是它位于您的应用程序脚本容许的文件夹中 - 那么它可使用 include_once 或 require 或 require_once 引入这个文件。若是不是--那么就不引入它。

如何进行检查?这很简单。你只须要将 $folder (文件)路径与一个容许程序引入文件 ( $file ) 的路径文件夹进行比较。

// 很差的例子,不要用!
if (substr($file, 0, strlen($folder)) === $folder) {
  include $file;
}

若是  $folder 的存放路径是 /path/to/folder  并且  $file  的存放路径是  /path/to/folder/and/file , 而后咱们在代码中使用 substr() 函数把他们的路径都变成字负串进行判断,若是文件位于不一样的文件夹中---这个字符串将不相等。反之则反。

上面的代码有两个重要的问题。若是 file 路径是 /path/to/folderABC/and/file,很明显,该文件也不在容许引入的文件夹中。经过向两个路径添加斜杠能够防止这种状况。咱们在这里向文件路径添加斜杠并不重要,由于咱们只须要比较两个字符串。

举个例子: 若是 folder 路径是  /path/to/folder  而且 file 路径是 /path/to/folder/and/file ,那么从 file 提取和 folder 具备相同数量的字符,那么 $ folder 将是 /path/to/folder

再好比 folder 路径是 /path/to/folder 而且 file 路径是 /path/to/folderABC/and/file, 那么从 file 中提取 folder 具备相同数量的字符,和 $folder同样,而且将再次成为/path/to/folder,这种都是错误的,这不是咱们指望的结果。

所以,在 /path/to/folder/ 添加斜杠后,与 /path/to/folder/and/file 的提取部分 /path/to/folder/ 相同就是安全的。

若是将 /path/to/folder//path/to/folderABC/and/file 的提取部分 / path/to/folderA ,很明显二个字符串不同。

这就是咱们指望获得的。但还有另外一个问题。这并不明显。我敢确定,若是我问你,你看到这里有一个灾难性的漏洞 - 你不会猜到它在哪里。你也许已经在经验中使用过这个东西,甚至可能就在今天。如今,您将看到漏洞是如何隐晦和显而易见。往下看。

/../

假想一个很常见的场景。

有这么一个网站。用户能够上传文件到该站点。全部的文件都位于一个特定的目录下。有一个包含用户文件的脚本。脚本自上而下进行查找是否包含用户的输入(直接或间接)路径---那这个脚本能够经过以下方式进行路径伪造:

/path/to/folder/../../../../../../../another/path/from/root/

举例。用户发起请求,你的脚本中包含了一个基于相似以下用户输入路径的文件:

include $folder . "/" . $_GET['some']; // or $_POST, or whatever

你麻烦大了。有天用户发送一个 ../../../../../../etc/.passwd 这种或其余请求,你就哭吧。

再否则。假若有人让你的脚本加载一个他想要的文件,你就废了。它不必定就只是出如今用户文件中。它多是你的CMS或你本身文件的一些插件(别相信任何人),甚至是应用程序逻辑中的错误等。

或者

用户可能会上传一个名为 file.php 的文件,你会把它和其余的用户文件同样放在一个特定的文件夹里面:

move_uploaded_file($filename, $folder . '/' . $filename);

用户的文件就存放在那里,你必须经常检查历来没有包含该文件夹中的文件,目前来看,全部的东西都挺正常的。一般,用户发给你的文件不会包含斜杠或者其余特殊字符,由于这是被系统文件系统禁止的。之因此这样,是由于一般状况下浏览器发给你的文件是在真实文件系统中建立的,同时它的名字是一些真实存在的文件的名字。

可是 http 请求容许用户发送任何字符。因此若是某人伪造请求建立名为 ../../../../../../var/www/yoursite.com/index.php 的文件---这行代码会覆盖你的 index.php 文件,若是 index.php 处于在上述路径的话。

全部的初学者都但愿经过过滤 「..」或者斜杠来解决这个问题,可是这种作法是错误的,因为你在安全方面还缺少经验。同时你必须(是的,必须)明白一个简单的事情:你永远没法在安全和密码学方面的得到足够的知识。这句话的意思是,若是你懂得了「两个点和斜杠」的漏洞,但这不表明你知道全部其余的缺陷、攻击和其余特殊字符,你也不知道在文件写入文件系统或数据库时可能发生的代码转换。

解决方案和答案

为了解决这个问题,PHP中内置了一些特殊函数方法,只是为了在这种状况下使用。

basename()

第一个解决方案 --- basename() 它从路径结束时提取路径的一部分,直到它遇到第一个斜杠,但忽略字符串末尾的斜杠,参见示例。不管如何,你会收到一个安全的文件名。若是你以为安全 - 那么是的这很安全。若是它被不法上传利用 - 你可使用它来校验文件名是否安全。

realpath()

另外一个解决方案 --- realpath()它将上传文件路径转换规范化的绝对路径名,从根开始,而且根本不包含任何不安全因素。它甚至会将符号连接转换为此符号连接指向的路径。

所以,您可使用这两个函数来检查上传文件的路径。要检查这个文件路径究竟是否真正属于此文件夹路径。

个人代码

我编写了一个函数来提供如上的检查。我并非专家,因此风险请自行承担。代码以下。

<?php
/**
 * Example for the article at medium.com
 * Created by Igor Data.
 * User: igordata
 * Date: 2017-01-23
 * @link https://medium.com/@igordata/php-running-jpg-as-php-or-how-to-prevent-execution-of-user-uploaded-files-6ff021897389 Read the article
 */
/**
 * 检查某个路径是否在指定文件夹内。若为真,返回此路径,不然返回 false。
 * @param String $path 被检查的路径
 * @param String $folder 文件夹的路径,$path 必须在此文件夹内
 * @return bool|string 失败返回 false,成功返回 $path
 *
 */
function checkPathIsInFolder($path, $folder) {
    if ($path === '' OR $path === null OR $path === false OR $folder === '' OR $folder === null OR $folder === false) {
      /* 不能使用 empty() 由于有可能像 "0" 这样的字符串也是有效的路径 */
        return false;
    }
    $folderRealpath = realpath($folder);
    $pathRealpath = realpath($path);
    if ($pathRealpath === false OR $folderRealpath === false) {
        // Some of paths is empty
        return false;
    }
    $folderRealpath = rtrim($folderRealpath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
    $pathRealpath = rtrim($pathRealpath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
    if (strlen($pathRealpath) < strlen($folderRealpath)) {
        // 文件路径比文件夹路径短,那么这个文件不可能在此文件夹内。
        return false;
    }
    if (substr($pathRealpath, 0, strlen($folderRealpath)) !== $folderRealpath) {
        // 文件夹的路径不等于它必须位于的文件夹的路径。
        return false;
    }
    // OK
    return $path;
}

结语。

  • 必须过滤用户输入,文件名也属于用户输入,因此必定要检查文件名。记得使用 basename() 。
  • 必须检查你想存放用户文件的路径,永远不要将这个路径和应用目录混合在一块儿。文件路径必须由某个文件夹的字符串路径,以及 basename($filename) 组成。文件被写入以前,必定要检查最终组成的文件路径。
  • 在你引用某个文件前,必须检查路径,而且是严格检查。
  • 记得使用一些特殊的函数,由于你可能并不了解某些弱点或漏洞。
  • 而且,很明显,这与文件后缀或 mime-type 无关。JPEG 容许字符串存在于文件内,因此一张合法的 JPEG 图片可以同时包含合法的 PHP 脚本。

不要信任用户。不要信任浏览器。构建彷佛全部人都在提交病毒的后端。

固然,也没必要惧怕,这其实比看起来的简单。只要记住 “不要信任用户” 以及 “有功能解决此问题” 即可。

转自 PHP / Laravel 开发者社区 https://laravel-china.org/top...
相关文章
相关标签/搜索