4.12 文件上传相关的问题
有些 Web 应用能让用户上传并公开图像文件或 PDF 文档。而本节就将讲述用户上传或下载文件时容易产生的安全隐患。
4.12.1 文件上传问题的概要
针对文件上传功能的攻击类型有如下几种。
- 针对上传功能的 DoS 攻击
- 使上传的文件在服务器上作为脚本执行
- 诱使用户下载恶意文件
- 越权下载文件
下面我们就来依次看一下上述的各种攻击类型。
- 针对上传功能的 DoS 攻击
使用 Web 应用的上传功能连续发送体积巨大的文件时,就可能会形成使网站负荷过载的 DoS 攻击(Denial of Service Attack,拒绝服务攻击)。
图 4-102 针对上传功能的 DoS 攻击
DoS 攻击会造成应用的响应速度下降,严重时还会造成服务器宕机等。
防范 DoS 攻击的一种有效策略为限制上传文件的容量。PHP 能够在 php.ini 中设置上传功能的容量限制。表 4-19 中列出了与文件上传相关的配置项。建议在满足应用需求的前提下尽量将值设置得小一些。如果应用不提供文件上传功能,那么只需将 file_uploads 设为 Off 即可。
详情请参考 PHP 的官方文档( http://php.net/manual/zh/ini.core.php )。
表 4-19 php.ini 中与文件上传相关的配置项
设置项目名 解说 默认值 file_uploads 是否允许使用文件上传功能 On upload_max_filesize 单个文件的最大容量 2MB max_file_uploads 单次请求最大文件上传个数 20 post_max_size POST 请求正文的最大限制 8MB memory_limit 脚本所能申请到的最大内存值 128MB 另外,设置 Apache 的 httpd.conf 也能限制请求正文的最大字节数。而且此设置也适用于 PHP 以外的情况。通过在前期的检验中将不合法的请求拒之门外,能够有效提高防御 DoS 的能力。以下设置为将请求正文限制在 100K 以内 63 。
LimitRequestBody 102400
关于 PHP 和 Apache 以外的工具中限制上传文件容量的设置方法,请参考相关的文档。
专栏:内存使用量与 CPU 使用时间等其他需要关注的资源
前面提到的内容都只是校验了上传文件的容量,而为了能够更好地防御 DoS 攻击,还应该 对其他参数也进行校验。例如,在服务器上处理图像文件时,比起压缩后图像文件的大小,解压后图像所占用的内存容量更容易出问题。
因此,为了能够正确估算解压后的内存使用量,我们就不能仅仅着眼于所接收的文件大小,还需要确定图像的尺寸及色数的上限值,并尽量在早期进行校验处理。
同样,在执行使 CPU 负担过重的处理时,也需要事先对 CPU 资源(CPU 的使用时间和执行时间)进行估算,并限制相关的参数。
- 使上传的文件在服务器上作为脚本执行
如果用户上传的文件被保存在 Web 服务器的公开目录中,外界上传的脚本文件就有可能在 Web 服务器上被执行。
图 4-103 在服务器上执行上传的脚本
如果执行了外界传入的脚本,就会造成与 4.11 节讲述的 OS 命令注入攻击同样的影响。具体表现为,信息被泄漏、文件被篡改、其他服务器遭到攻击等。详情请参考 4.12.2 节。
- 诱使用户下载恶意文件
针对文件上传功能的第 3 种攻击方式为上传恶意文件并诱使用户下载,一旦用户浏览了该恶意文件,其 PC 就会执行 JavaScript 脚本或者感染病毒等。
图 4-104 诱使用户下载恶意文件
然而,这时可能会有读者产生这样的疑问,用户只是下载了文件,怎么会造成 JavaScript 脚本被执行呢?这是因为攻击者能通过一些手段使浏览器将其上传的文件误认为是 HTML。详情 将在 4.12.3 节中进行说明。
此外,下载文件会导致 PC 感染病毒则是因为攻击者恶意利用了用来打开文件的软件中存在的漏洞。
下载文件造成病毒感染,虽然直接原因在于上传恶意文件的用户,但有时网站的运营方也负有一定责任。因此,在决定网站的服务内容时,应当根据网站的性质决定是否对恶意软件采取措施。详情请参考 7.4 节。
- 越权下载文件
即使上传后的文件只允许特定的用户下载,有时也会出现没有下载权限的用户也能够下载文件的问题。此类问题的原因多数为没有对文件设置访问权限,从而导致用户通过推测 URL 而成功下载到了没有下载权限的文件。
此问题将在 5.3 节中详述。
63 http://httpd.apache.org/docs/2.2/en/mod/core.html#limitrequestbody
4.12.2 通过上传文件使服务器执行脚本
概要
有些文件上传处理会将用户上传的文件保存至 Web 服务器的公开目录中。这时,如果应用中允许上传文件的扩展名为 php、asp、aspx、jsp 等脚本文件的扩展名,用户就能在服务器上将上传的文件作为脚本执行。
如果外界传入的脚本在服务器上被执行,就会造成与 OS 命令注入同样的影响,具体如下。
- 浏览、篡改或删除 Web 服务器内的文件
- 对外发送邮件
- 攻击其他服务器(称为垫脚石)
为了防范通过上传文件而在服务器上执行脚本,可以综合实施以下两种方法,或者实施其中的任意一种。
- 不将用户上传的文件保存在公开目录中,浏览文件需通过脚本
- 将文件的扩展名限定为不可执行的脚本文件
通过上传文件使服务器执行脚本的安全隐患总览
攻击手段与影响
接下来我们就来看一下通过上传文件而使服务器端执行脚本的攻击模式及其影响。
- 示例脚本解说
以下为使用户上传图像文件并将该图像在页面上显示出来的 PHP 脚本。首先来看文件上传页面。可以看出,上传文件的 form 元素的 enctype 属性被指定为了
"multipart/form-data"
。代码清单 /4c/4c-001.php
<body> <form action="4c-002.php" method="POST" enctype="multipart/form-data"> 文件 :<input type="file" name="imgfile" size="20"><br> <input type="submit" value=" 上传 "> </form> </body>
而以下脚本的作用就是接收文件后将其保存在 /4c/img/ 目录中,并在页面上显示出来。
代码清单 /4c/4c-002.php
<?php $tmpfile = $_FILES["imgfile"]["tmp_name"]; // 临时文件名 $tofile = $_FILES["imgfile"]["name"]; // 原文件名 if (! is_uploaded_file($tmpfile)) { // 判断文件是否已经上传 die(' 文件没有上传 '); // 将图像文件移动至 img 目录 } else if (! move_uploaded_file($tmpfile, 'img/' . $tofile)) { die(' 无法上传文件 '); } $imgurl = 'img/' . urlencode($tofile); ?> <body> <a href="<?php echo htmlspecialchars($imgurl); ?>"><?php echo htmlspecialchars($tofile, ENT_NOQUOTES, 'UTF-8'); ?></a> 已上传 <br> <img src="https://www.wenjiangs.com/wp-content/uploads/2024/10/<?php echo htmlspecialchars($imgurl); ?>"> </body>
正常情况下的执行过程如下所示。
图 4-105 示例脚本的执行范例(正常情况)
专栏:警惕文件名中的 XSS
4c-002.php 中生成图像文件的 URL 时,会通过 urlencode 函数对文件名进行百分号编码,并在显示处理中执行 HTML 转义。这些都是必要的处理。Unix 允许在文件名中使用 <、>、" 等字符,因此需要根据所在位置进行相应的转义处理。当然这些都不是新鲜的内容,只是照理实施 XSS 的防范策略而已。
- PHP 脚本的上传与执行
下面就让我们来看一下攻击的例子。这里假设用户上传的不是图像文件,而是以下 PHP 脚本文件。
代码清单 4c-900.php
<pre> <?php system('/bin/cat /etc/passwd'); ?> </pre>
这段 PHP 脚本的作用在于通过
system
函数调用系统命令 cat 来显示 etc/passwd 文件的内容。上传该 PHP 脚本文件后,浏览器的页面显示如下图所示。由于 4c-900.php 并非标准的图像文件,因此页面上显示为红叉。图 4-106 上传了 PHP 脚本
接下来点击 4c-900.php 链接,就能使浏览器显示刚才上传的 PHP 脚本文件。如图 4-107 所示,点击后页面上显示了 etc/passwd 文件的内容。由此可以得知上传的 PHP 脚本在服务器上被成功执行了。
图 4-107 上传的 PHP 脚本在服务器上被执行
上传的脚本文件在服务器上被执行造成的影响与 OS 命令注入相同。由于
system
和passthru
等函数都能用来调用 OS 命令,因此攻击者就能够执行当前操作系统账号权限范围内的所有操作。
安全隐患的产生原因
上传的文件能被作为脚本执行这一安全隐患的产生需满足如下两项条件。
- 上传的文件被保存至公开目录
- 上传后的文件扩展名能被指定为 .php 或 .asp 等表示脚本的扩展名
如果应用中的上传功能满足了上述两项条件,就会滋生安全隐患。因此,防范策略为至少消除上述两项条件中的任意一项。
对策
正如前项所介绍的那样,用户上传的文件能被作为脚本执行的条件为以下两项:文件被保存在公开目录中以及用户能指定文件扩展名为可执行的脚本文件。因此,消除上述任意一项条件就能防范安全隐患。而考虑到如果仅限制文件的扩展名很有可能会产生疏漏,因此,这里我们将主要介绍另一种对策方法,即不将文件保存在公开目录中。
为了避免将上传的文件保存在公开目录中,下载文件时就需要经过脚本。本书把此类脚本称为“下载脚本”。
使用下载脚本将 4c-002.php 加以改良,结果如下所示。
代码清单 /4c/4c-002a.php
<?php function get_upload_file_name($tofile) { /* 省略 */ } $tmpfile = $_FILES["imgfile"]["tmp_name"]; $orgfile = $_FILES["imgfile"]["name"]; if (! is_uploaded_file($tmpfile)) { die(' 文件没有上传 '); } $tofile = get_upload_file_name($orgfile); if (! move_uploaded_file($tmpfile, $tofile)) { die(' 无法上传文件 '); } $imgurl = '4c-003.php?file=' . basename($tofile); ?> <body> <a href="<?php echo htmlspecialchars($imgurl); ?>"><?php echo htmlspecialchars($orgfile, ENT_NOQUOTES, 'UTF-8'); ?></a> 已上传 <br> <img src="https://www.wenjiangs.com/wp-content/uploads/2024/10/<?php echo htmlspecialchars($imgurl); ?> "> </body>
可以看出,上述脚本对原先脚本做了 2 处修改。首先,将文件的保存场所从公开目录(/4c/img)改为了由 get_upload_file_name
函数返回的文件名。另外,取得图像的 URL 时使其经过了下载脚本。 get_upload_file_name
函数的源码如下所示。
代码清单 /4c/4c-002a.php(get_upload_file_name 的定义)
define('UPLOADPATH', '/var/upload');
function get_upload_file_name($tofile) {
// 校验扩展名
$info = pathinfo($tofile);
$ext = strtolower($info['extension']); // 扩展名(统一为小写字母)
if ($ext != 'gif' && $ext != 'jpg' && $ext != 'png') {
die(' 只能上传扩展名为 gif、jpg 或 png 的图像文件 ');
}
// 下面的处理为生成唯一的文件名
$count = 0; // 尝试生成文件名的次数
do {
// 生成文件名
$file = sprintf('%s/%08x.%s', UPLOADPATH, mt_rand(), $ext);
// 生成文件,如果文件已存在则报错
$fp = @fopen($file, 'x');
} while ($fp === FALSE && ++$count < 10);
if ($fp === FALSE) {
die(' 无法生成文件 ');
}
fclose($fp);
return $file;
}
get_upload_file_name
函数中首先确保文件的扩展名为 gif、jpg 或 png。
接着,利用随机数生成包含了原来扩展名的唯一的文件名,并检验文件名是否有重复 64 。文件名被生成后,再使用指定了 'x'
选项的 fopen
来打开文件,这样当文件已经存在时就会进入错误处理。出错后会不断循环执行 fopen
直到不出错为止,但考虑到还存在文件名冲突以外的异常而导致出错的情况,因此,如果生成文件名处理超过 10 次的话就中止此处理。
64 此处将 PHP 的官方文档中的示例脚本 http://www.php.net/manual/zh/function.tempnam.php#98232 进行了改良。
随后将文件关闭,但不删除生成的文件,而是通过 move_uploaded_file
函数覆盖原文件。如果将文件删除,就无法保证文件名的唯一性。
下面为下载脚本 4c-003.php 的源码。
代码清单 /4c/4c-003.php
<?php
// 注意 :该下载脚本中包含跨站脚本漏洞
//
define('UPLOADPATH', '/var/upload');
$mimes = array('gif' => 'image/gif', 'jpg' => 'image/jpeg',
'png' => 'image/png',);
$file = $_GET['file'];
$info = pathinfo($file); // 取得文件信息
$ext = strtolower($info['extension']); // 扩展名(统一为小写字母)
$content_type = $mimes[$ext]; // 取得 Content-Type
if (! $content_type) {
die(' 只能上传扩展名为 gif、jpg 或 png 的图像文件 ');
}
header('Content-Type: ' . $content_type);
readfile(UPLOADPATH . '/' . basename($file));
?>
上述脚本是从查询字符串 file
中取得文件名的。首先获取扩展名,如果不是 gif、jpg 或 png 就报错。接着输出与各扩展名相对应的 Content-Type,然后再使用 readfile
函数读取文件内容并将其输出。这里将从查询字符串中取得的文件名经过 basename
函数进行处理是为了防范目录遍历漏洞(参考 4.10 节)。
实施以上防范策略之后,用户上传的文件在服务器端被作为脚本执行这一安全隐患就能够得以消除。但是,如果用户使用的是 Internet Explorer(IE)浏览器,上述脚本就有遭到跨站脚本攻击的风险。此问题将在下一节讲述。
专栏:校验扩展名时的注意点
为了防范通过上传文件而使服务器执行脚本,本书介绍了使用下载脚本的方法。而如果只是为了防范文件被当作脚本执行,也能够采取校验文件扩展名的方法,只是实施周密的校验并不容易。
举例来说,使用名为 SSI(Server Side Include)的功能就能将 HTML 中引入(Include)的文件当作命令(Command)执行。虽然使用 SSI 的 HTML 文件的标准扩展名为 shtml,但是有时通过设置也能使扩展名为 html 的文件允许 SSI 功能。换言之,有些情况下也需要把扩展名为 html 的文件视为脚本文件。
由此可见,应该将哪些扩展名归类为可执行的脚本文件是不确定的。因此,校验扩展名时推荐只允许所需的最低限度。另外,如果没有特殊理由,还是推荐使用下载脚本的方法来加以应对。
4.12.3 文件下载引起的跨站脚本
概要
当用户下载已上传的文件时,浏览器有时会不能正确识别文件的类型。比如,尽管应用中认定某文件为 PNG 格式,但如果该图像文件的数据中包含 HTML 标签,在某些条件下浏览器就会将其误认为 HTML 文件,从而便会执行图像文件中的 JavaScript。这就是文件下载引起的跨站脚本(XSS)。
攻击者会通过上传嵌入 HTML 或 JavaScript 的图像文件或 PDF 文件来对此漏洞发起攻击。虽然用常规的方法阅览时,这些恶意文件并不会被识别为 HTML,但是攻击者会使用一些伎俩促使上传的文件被识别为 HTML。而一旦用户的浏览器将文件识别为 HTML,XSS 攻击就成功了。
文件下载引起的 XSS 攻击所造成的影响,与 4.3.1 节讲述的影响一样。
为此,可通过采取如下对策来防范文件下载引起的 XSS 漏洞。
- 正确设置文件的 Content-Type
- 确保图像文件的扩展名与内容(图像文件头)相符
- 判定为用于下载的文件时,在响应头中指定 Content-Disposition:attachment
文件下载引起的 XSS 漏洞总览
攻击手段与影响
接下来就让我们首先看一下两种利用文件下载的 XSS 攻击的手段。这里介绍的攻击方法能在 Internet Explorer(IE)中重现,而使用 IE 以外的浏览器则不一定能够重现,但由于 IE 的市场份额很高,而且使用此处介绍的方法开发的应用也同样适用于其他浏览器,因此这里我们以 IE 浏览器为例来进行讲述。
- 图像文件引起的 XSS
在某些情况下,将包含 HTML 或 JavaScript 代码的文件伪装成图像文件上传,就可能会形成跨站脚本(XSS)攻击。而且通过下面展示的例子也可以看到,即使已经实施了相应的对策来防止用户上传的脚本在服务器端被执行,在下载文件的时候还是有可能会遭受跨站脚本攻击。
虽然 IE8 及以后的版本中已经对利用图像的 XSS 攻击进行了防范,但考虑到 IE7 及之前的版本还有一定数量的用户,因此在应用中采取防范措施还是很有必要的。
在试验环境中打开 http://example.jp/4c/4c-001a.php ,或者在 http://example.jp/4c/ 的菜单中点击“2. 4c-001a: 文件上传(经过下载脚本)”链接,该上传页面已经实施过针对执行脚本的防范对策。
由于页面上会要求输入文件名,因此这里我们新建以下文本文件并将文件命名为 4c-901.png 后保存,然后再在页面上指定此文件名。
代码清单 4c-901.png
<script>alert('XSS');</script>
完成上传后,页面显示如下。由于 4c-901.png 并非标准的图像文件,因此页面上显示了一个红叉的记号。
图 4-108 上传了伪装成图像的文件
这时,点击 4c-901.png 链接,先前的伪装图像就会直接显示出来。如下图所示,IE7 执行了 JavaScript 代码,而 IE8 则只显示了文本信息。
图 4-109 在 IE7 及之前的版本中 XSS 攻击成功
在实际发动攻击时,攻击者上传包含恶意 JavaScript 代码的图像文件以后,还会将显示此图像的 URL 添加到恶意网站中。然而,由于使用 img 元素显示图像时 JavaScript 不会被执行,因此攻击者通常利用 iframe 等元素来让它以 HTML 的形式显示。
而 JavaScript 被执行后,网站的 HTTP 消息就如下图的 Fiddler 界面所示。
图 4-110 被执行 JavaScript 的网站的 HTTP 消息
可以看出 HTTP 响应中的 Content-Type 消息头准确无误地指定为了 image/png。然而 IE7 却对此视而不见,仍然将该响应判断为 HTML 类型,从而也就导致了 JavaScript 被执行。
利用图像文件的 XSS 所造成的影响与 4.3 节介绍的普通的 XSS 相同,即 Cookie 被窃取而造成伪装攻击、Web 功能被恶意使用、页面被篡改进而导致钓鱼攻击等。
- PDF 下载引起的 XSS
除了图像服务网站之外,下面我们再来看一个提供 PDF 等应用文件下载服务的网站案例。这里的示例网站也就相当于存储服务网站的简略版。
- 示例脚本解说
首先我们来看一下示例脚本。该试验中的文件上传页面(4c-011.php)基本上直接沿用了 4c-001.php,只是将 action 的目标改为了 4c-012.php。
同样,在接收上传文件的页面(4c-012.php)和下载脚本(4c-013.php)中,将接收文件的类型更改为了 PDF。
代码清单 /4c/4c-012.php(开头和末尾)
<?php define('UPLOADPATH', '/var/upload'); function get_upload_file_name($tofile) { // 校验扩展名 $info = pathinfo($tofile); $ext = strtolower($info['extension']); // 扩展名(统一为小写字母) if ($ext != 'pdf') { die(' 只能上传扩展名为 pdf 的文件 '); } // ... 中略 $imgurl = '4c-013.php?file=' . basename($tofile); ?> <body> <a href="<?php echo htmlspecialchars($imgurl); ?>"><?php echo htmlspecialchars($orgfile, ENT_NOQUOTES, 'UTF-8'); ?> 已上传 </a><br> </body>
下面是下载脚本的源码。阴影部分为与 4c-003.php 的不同之处。
代码清单 /4c/4c-013.php
<?php define('UPLOADPATH', '/var/upload'); $mimes = array('pdf' => 'application/x-pdf'); $file = $_GET['file']; $info = pathinfo($file); // 取得文件信息 $ext = strtolower($info['extension']); // 扩展名(统一为小写字母) $content_type = $mimes[$ext]; // 取得 Content-Type if (! $content_type) { die(' 只能上传扩展名为 pdf 的文件 '); } header('Content-Type: ' . $content_type); readfile(UPLOADPATH . '/' . basename($file)); ?>
首先看到的是正常情况下的页面跳转。在页面 4c-011.php 上指定恰当的 PDF 文件后点击上传按钮,页面显示如下。
图 4-111 上传 PDF 文件后的页面
这时点击下载链接就能下载 PDF 文件,页面显示如下图所示。
图 4-112 点击链接后下载 PDF
- 将 HTML 文件伪装成 PDF 而引起的 XSS
下面我们就不再使用正常的 PDF 文件,而是将仅包含 script 元素的 HTML 文件命名为 4c-902.pdf 后保存,然后再通过刚才的脚本(4c-011.php)将其上传。
代码清单 4c-902.pdf
<script>alert('XSS');</script>
上传此伪装 PDF 文件后,页面显示如下图所示。这时点击“4c-902.pdf 上传完毕”链接就会出现下载文件的对话框。
而以下就是攻击者生成恶意链接的手段。右击下载使用的链接,选择菜单中的“复制快捷方式”。
图 4-113 选择菜单中的“复制快捷方式”
接下来,将快捷方式(URL)粘贴在浏览器的地址栏上。这时理论上应该会出现类似于下面的 URL,但由于 file= 后面的文件名是随机生成的,因此在读者的环境中应该会显示为其他字符串。
http://example.jp/4c/4c-013.php?file=1af12536.pdf
此时,将字符串 /a.html 插入到 URL 中,如下面的阴影部分所示。插入的字符串被称为 PATHINFO,这是以貌似文件名的形式将参数添加到 URL 中的方法。由于文件 a.html 实际上并 不存在,因此该字符串会被作为参数传递给 4c-013.php 脚本。
http://example.jp/4c/4c-013.php/a.html ?file=1af12536.pdf
如果这时按下回车键,如下图所示,JavaScript 就会被执行。与伪装图像的情况不同,IE7 和 IE8 中都执行了 JavaScript 代码。
图 4-114 XSS 攻击成功
由此可见,将 HTML(JavaScript)文件伪装成 PDF 并上传后,只要在调用该文件的 URL 中添加 PATHINFO,就能使得攻击对象网站执行 JavaScript。
- 漏洞的根本原因是 Content-Type 不正确
伪装 PDF 之所以会引起 XSS 漏洞,其根本原因在于 Content-Type 有误。PDF 正确的 Content-Type 为 application/pdf,而如果 Content-Type 被错误地设置为了 application/x-pdf`,就会直接导致漏洞的产生。
- 示例脚本解说
安全隐患的产生原因
文件下载之所以会引起 XSS 是因为受到了 Internet Explorer 特性的影响。Internet Explorer 中判断文件类型时,除了基于 HTTP 响应的 Content-Type 消息头以外,还会参考 URL 中的扩展名和文件的内部数据。虽然具体的判断方法并没有对外公开,但目前能够得知的内部行为如下。
- 内容为图像时
文件内容为图像的情况下,IE 判断文件类型时除了基于响应头中的 Content-Type,还会用到图像文件的文件头。图像文件头是指位于文件开头的固定字符串,一般被用来识别文件类型。GIF、JPEG65 、PNG 的文件头如下表所示。
表 4-20 图像文件的文件头
图像格式 文件头 GIF GIF87a 或 GIF89a JPEG \xFF\xD8\xFF PNG \x89PNG\x0D\x0A\x1A\x0A Internet Explorer(7 及以前版本)中默认按照以下方法判断文件类型。
- Content-Type 和文件头一致时
这时采用 Content-Type 所示的文件类型。
- Content-Type 和文件头不一致时
Content-Type 和文件头不一致时,两者都会被浏览器忽略。这时浏览器会根据文件的内容来推测文件类型。如果文件中包含 HTML 标签,该文件就可能会被判定为 HTML 文件 66 。在“图像文件引起的 XSS”这一小节中介绍的伪装 PNG 文件就属于这类情况。该示例文件中虽然没有包含图像文件头,但根据笔者的试验,即使添加了图像文件头,如果与 Content-Type 矛盾也会被浏 览器无视 67 。
- Content-Type 和文件头一致时
- 内容不为图像时
图像文件以外的情况下,各 IE 版本都做如下处理。首先,根据浏览器是否能够处理接收到的 Content-Type,IE 的举动会有所不同。
如果 IE 能够处理收到的 Content-Type,就会按照 Content-Type 来处理。注册表 HKEY_CLASSES_ROOT\MIME\Database\Content Type 中保留了 IE 能够处理的所有 Content-Type。图 4-115 中列出了其中的一部分。如图所示,PDF 的 Content-Type 为 application/pdf,而非 application/x-pdf。
图 4-115 IE 能够处理的 Content-Type
如果收到的 Content-Type 不是 IE 能够处理的类型,那么 IE 就会根据 URL 中的扩展名进行判断。判断规则的详情非常复杂,有兴趣的读者可以参考长谷川阳介的文章《无法忽视:IE 中对 Content-Type 的忽视》[1]。在上面介绍的“将 HTML 文件伪装成 PDF 而引起的 XSS”这一小节中,生成用来攻击的 URL 时添加了作为 PATHINFO 的 /a.html,这就是恶意利用了 IE 会通过 URL 中的扩展名来判断文件类型的特性。
65 JPEG 本来是图像压缩方法的名称,作为文件格式时的术语应该为 JFIF,然而由于 JPEG 也普遍被用来指代 JFIF 文件格式,因此本书也采用 JPEG 这个称呼。
66 在以前(IE7 为止)的版本中,当文件满足上述条件时会被判定为 HTML 文件,而从 IE8 开始,满足上述同等条件的文件则会被视为文本文件(text/plain)。
67 详情请参考笔者的博客文章《图像文件引起跨站脚本(XSS)的倾向与对策》[2]。
对策
应对文件下载所引起的 XSS 漏洞的方法可分为上传时的对策和下载时的对策,分别如下。
- 文件上传时的对策
上传文件时实施以下操作。
- 校验扩展名是否在允许范围内
- 图像文件的情况下确认其文件头
关于校验扩展名,4.12.2 节的对策已经详述过。PHP 可以使用
getimagesize
函数来确认图像的文件头。格式清单 getimagesize 函数
array getimagesize(string $filename [, array &$imageinfo])
该函数将接收到的图像文件的文件名作为参数,并以数组的形式返回图像的长宽尺寸和图像格式等信息。下面是一些常见的图像格式所对应的整数值和常量。详情请参考 PHP 的文档 68 。
表 4-21 getimagesize 函数返回的图像格式信息
值 常量 1 IMAGETYPE_GIF 2 IMAGETYPE_JPEG 3 IMAGETYPE_PNG 在之前的介绍中,我们已经了解到图像上传脚本的改良版 4c-002a.php 中存在 XSS 漏洞。而使用
getimagesize
函数就可以消除 XSS 漏洞。假设改良后的脚本名为 4c-002b.php。检验图像文件的函数check_image_type
的定义如下。代码清单 /4c/4c-002b(check_image_type 函数的定义)
// function check_image_type($imgfile, $tofile) // $imgfile : 校验对象的图像文件名 // $tofile : 文件名(用于校验扩展名) function check_image_type($imgfile, $tofile) { // 取得并校验扩展名 $info = pathinfo($tofile); $ext = strtolower($info['extension']); // 扩展名(统一为小写字母) if ($ext != 'png' && $ext != 'jpg' && $ext != 'gif') { die(' 只能上传扩展名为 gif、jpg 或 png 的图像文件 '); } // 取得图像类型 $imginfo = getimagesize($imgfile); // 取得图像信息的数组 $type = $imginfo[2]; // 取出图像类型 // 下面,如果是正常的组合就 return if ($ext == 'gif' && $type == IMAGETMPE_GIF) return true; if ($ext == 'jpg' && $type == IMAGETMPE_JPEG) return true; if ($ext == 'png' && $type == IMAGETMPE_PNG) return true; // 如果到最后都没有 return 就报错 die(' 扩展名和图像类型不一致 '); }
下面为调用上述
check_image_type
函数的部分。阴影部分即为添加的代码行。代码清单 /4c/4c-002b.php
$tmpfile = $_FILES["imgfile"]["tmp_name"]; $orgfile = $_FILES["imgfile"]["name"]; if (! is_uploaded_file($tmpfile)) { die(' 文件没有上传 '); } // 校验图像 check_image_type($tmpfile, $orgfile); $tofile = get_upload_file_name($orgfile);
专栏:BMP 格式的注意点与 MS07-057
本书中介绍了浏览器涉及的 3 种图像格式,即 GIF、JPEG 与 PNG,而有的浏览器也可以处理其他格式的图像文件。像 Windows 中的标准格式 BMP 也能够在主流的浏览器中显示。那么,遇到 BMP 格式时该如何处理呢?
其实上面介绍的方法并不能完美地处理 BMP 格式的图像。BMP 格式的图像文件头为 BM,但处理 BMP 图像时,即使 Content-Type 与文件头一致,IE 6 和 IE 7 中也有可能将其识别为 HTML 从而导致 JavaScript 被执行。
PNG 格式也曾经发生过与 BMP 相同的现象,但这个问题已经由 MS07-057 安全更新补丁(2007 年 10 月)所修复。由此可见,提醒用户安装最新的安全更新补丁是非常重要的。
另外,从实用性的角度来看,由于 BMP 很不适合压缩(只能使用单纯的压缩方式),并且 BMP 仅限于 Windows 使用,因此我们并没有必要非在互联网上使用 BMP 格式的文件。而需要使用 BMP 时也都可以用 PNG 格式来代替。
综上所述,这里不推荐大家在 Web 上使用 BMP 格式的文件。
- 文件下载时的对策
下载文件时的对策如下。
- 正确设置 Content-Type
- 图像文件的情况下确认其文件头
- 必要时设置 Content-Disposition 消息头
- 正确设置 Content-Type
在 PDF 文件下载所引起的 XSS 漏洞示例中,漏洞产生的主要原因均为 Content-Type 设置有误。因此,只要将 PDF 格式的 Content-Type 正确设置为 application/pdf,就能够消除漏洞。而且除 IE 之外,正确指定 Content-Type 这一对策也适用于其他所有的浏览器。
如果下载时不经过下载脚本而是将文件保存在公开目录中的话,就一定要确认 Web 服务器的设置是否有误。Apache 中,Content-Type 的设置被保存在了名为 mime.types 的配置文件中。PDF 等常见的软件一般不会有问题,而如果用到了很生僻的软件或自己设置 mime.types 时,请务必确保浏览器能够识别该 Content-Type。
- 图像文件的情况下确认其文件头
通过下载脚本来下载图像文件时,只要在下载时确认了文件头,即使由于某些原因 Web 服务器中混入了非法的图像文件,也不会影响到应用程序。
下面是实施了检验文件头对策的改良版的下载脚本(摘要)。阴影部分中调用了检验文件头的函数
check_image_type
。代码清单 /4c/4c-003b.php
<?php define('UPLOADPATH', '/var/upload'); // function check_image_type($imgfile, $tofile) // $imgfile : 校验对象的图像文件名 // $tofile : 文件名(用于校验扩展名) function check_image_type($imgfile, $tofile) { /* 省略 */ } $mimes = array('jpg' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif'); $file = $_GET['file']; $info = pathinfo($file); // 取得图像类型 $ext = strtolower($info['extension']); // 扩展名(统一为小写字母) $content_type = $mimes[$ext]; // 取得 Content-Type if (! $content_type) { die(' 只能上传扩展名为 gif、jpg 或 png 的图像文件 '); } $path = UPLOADPATH . '/' . basename($file); check_image_type($path, $path); header('Content-Type: ' . $content_type); readfile($path); ?>
- 必要时设置 Content-Disposition 消息头
当下载的文件并不需要使用应用程序打开,而是只要求能够下载就行的情况下,可以在响应消息头中指定 Content-Disposition: attachment。这时,如果将 Content-Type 设为 application/octetstream,文件类型就变成了“用于下载的文件”。下面为消息头的设置示例。
Content-Type: application/octet-stream Content-Disposition: attachment; filename="hogehoge.pdf"
这里,Content-Diaposition 消息头的选项属性 filename 被用于指定保存文件时的默认文件名。
- 其他对策
以上介绍的 XSS 对策是为了防范漏洞所需要进行的最低限度的校验处理。例如,仅校验图像文件头并无法确认是否真的能在用户的浏览器上显示。
因此在决定 Web 应用的详细规格时,还应当探讨是否要执行以下校验。
- 除了图像文件的大小之外还校验尺寸和色数等
- 校验文件是否能作为图像文件读取
- 扫描病毒(详情见 7.4 节)
- 校验文件内容(自动或手动)
- 成人内容
- 侵犯版权的内容
- 违反法律或妨害公共秩序的内容
- 其他
68 http://www.php.net/manual/zh/function.getimagesize.php
专栏:将图像托管在其他域名
2009 年左右,有些网站开始将图像托管在主服务域名之外的单独域名上。下面列举的就是一些将图像托管在其他域名的网站。
表 4-22 将图像托管在其他域名的网站案例
网站名
主域名
图像使用的域名
Yahoo ! JAPAN
yahoo.co.jp
yimg.jp
YouTube
youtube.com
ytimg.com
niconico 动画
nicovideo.jp
nimg.jp
twitter.com
twimg.com
Amazon.co.jp
amazon.co.jp-images
amazon.com
上面这些都是高流量的网站,虽然将图像使用的域名分离出来多是为了使网站的响应速度更快,但另一方面,这一操作也具有提升网站安全性的效果。
这是因为,将用户上传的图像或 PDF 等文件保存在其他域名后,即使图像文件造成的 XSS 攻击取得成功,也不会波及主服务。
下载时的 XSS 基本上属于浏览器的问题,由于这一问题在市场份额很高的 IE 中非常常见,而且至今尚未得到完全修复。因此,作为辅助性对策,最好考虑一下将图像存储在其他域名的方法。
参考:用户 PC 中没有安装对应的应用程序时
如果用户的 PC 中没有安装 Content-Type 所对应的应用程序,该 Content-Type 就会被浏览器视为“未知”,从而就可能会造成 XSS。
要处理此问题并不容易。但通过采取以下措施即可进行有效的防范。
- 托管文件的服务器使用其他域名
- 添加 Content-Disposition 消息头
然而,由于上述方法会产生副作用,因此建议采取以下方法,虽然可靠性略逊一筹但能保证没有副作用。
- 校验 URL 是否与应用中预想的一致
- 通知用户安装浏览文件所需的应用程序
总结
本节讲述了图像的上传与下载处理所引起的安全隐患。虽然上传处理造成的安全隐患一直以来都没有受到太大关注,但是,鉴于漏洞造成的影响较大,并且可照相手机的高速普及造成了照片分享网站的增加,此外存储服务网站也在快速增长,因此想必今后会有越来越多的 Web 应用需要警惕这个安全隐患。
文件上传与下载问题的基本对策为正确设置 Content-Type 和扩展名。图像文件的情况下,校验文件头是最起码的操作,此外,根据需要还应当校验图像文件的有效性。
参考文献
[1] はせがわようすけ .(2009 年 3 月 30 日). [ 無視できない ]IE の Content-Type 無視([ 无法忽视 ]IE 中对 Content-Type 的忽视). 参考日期:2010 年 10 月 13 日 . 参考网址:@IT : http://www.atmarkit.co.jp/ait/articles/0903/30/news118.html
[2] 德丸浩(2007 年 12 月 10 日). 画像ファイルによるクロスサイト · スクリプティング (XSS) 傾向と対策(图像文件引起跨站脚本(XSS)的倾向与对策). 参考日期:2010 年 10 月 13 日,参考网址:德丸浩の日記 : http://www.tokumaru.org/d/20071210.html
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论