每一个专业的 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 的方法引入其余须要运行的文件。bash
其实还有第三种方法: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 图片,会发生?服务器会执行仍是不呢?
图片来源: Echo / Cultura / Getty Images
程序不关心文件名。甚至不关心文件是否有名字,也不关心它到底是不是文件。
有至少两个状况可让PHP执行代码:
<?php
和 ?>
标记之间<?=
和 ?>
标记之间即便文件中填充了一些奇怪的二进制数据或一些奇怪的保护名称,该标记中的代码仍然会被执行。
这里有一个图片给您:
该图片没有问题
它如今很纯净。可是您可能知道 JPEG 格式容许在文件中添加一些注释。好比,拍摄照片的相机型号或坐标地址。若是咱们试图在里面放一些PHP代码并尝试 include 或 require 呢?让咱们来看看吧!
下载这个图片到你的硬盘上。或者你本身去弄一张 JPEG 图片也行。你随便用什么格式的文件都无所谓。我建议用一个 JPEG 文件来演示,主要是由于它是一张图片且易于在其中进行文本编辑。我用的是一个 Windows的笔记本,目前我手头上没有 Apple 或 Linux(或其余UNIX系的系统)的笔记本。因此一会我会发一个这个 OS 下的屏幕快照。可是我确信你确定也能作这个事。
用如下这段 PHP 代码建个文件:
<h1>Problem?</h1>
<img src="troll-face.jpg">
<?php
include "./troll-face.jpg";
复制代码
troll-face.jpg
若是你把你的 php 文件命名为 index.php
,而后把它放在文件根目录或者放在你网站目录下的任何一个文件目录中。
若是你准确完成了上述步骤,你就能够看到这个画面:
到此这都没毛病。没 PHP 代码展现,也没有 PHP 代码被执行。
如今,咱们来添加一个问题:
<?php echo "<h2>Yep, a problem!</h2>"; phpinfo(); ?>
复制代码
刷新页面!
很明显出现了一点问题!
您在页面上看到了该图片。相同的图片还存在页面的 PHP 代码中。图片的代码也被执行了。
长话短说: 若是咱们不在程序种引入这些不安全的文件,文件中的脚本就不会执行。
仔细看下面的例子。
若是有人在某处看到我错了 - 请纠正我,这是一个严重的问题。
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() 它从路径结束时提取路径的一部分,直到它遇到第一个斜杠,但忽略字符串末尾的斜杠,参见示例。不管如何,你会收到一个安全的文件名。若是你以为安全 - 那么是的这很安全。若是它被不法上传利用 - 你可使用它来校验文件名是否安全。
另外一个解决方案 --- 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($filename)
组成。文件被写入以前,必定要检查最终组成的文件路径。不要信任用户。不要信任浏览器。构建彷佛全部人都在提交病毒的后端。
固然,也没必要惧怕,这其实比看起来的简单。只要记住 “不要信任用户” 以及 “有功能解决此问题” 即可。
转自 PHP / Laravel 开发者社区 laravel-china.org/topics/1962…