第二章 表单及URL-php安全基础

本章主要讨论表单处理,同时还有在处理来自表单和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:

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

小提示
       欲更多了解常量, 请访问 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属性中指定请求方式:

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

      如果我输入了用户名chris和密码mypass,在表单提交后,我会到达URL为http://example.org/login.php?username=chris&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:

      如果我再次指定用户名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:

      可以看出,接收脚本reset.php会得到所有信息,包括重置哪个帐号的密码、并给出将新密码发送到哪一个邮件地址。
      如果一个用户能看到上面的表单(在回答正确问题后),你有理由认为他是chris帐号的合法拥有者。如果他提供了chris@example.org作为备用邮件地址,在提交后他将进入下面的URL:
CODE:
  http://example.org/reset.php?user=chris&email=chris%40example.org
      该URL出现在浏览器栏中,所以任何一位进行到这一步的用户都能够方便地看出其中的user和mail变量的作用。当意思到这一点后,这位用户就想到php@example.org是一个非常酷的地址,于是他就会访问下面链接进行尝试:
CODE:
  http://example.org/reset.php?user=php&email=chris%40example.org
      如果reset.php信任了用户提供的这些信息,这就是一个语义URL攻击漏洞。在此情况下,系统将会为php帐号产生一个新密码并发送至chris@example.org,这样chris成功地窃取了php帐号。
      如果使用session跟踪,可以很方便地避免上述情况的发生:
CODE:

      尽管上例省略了一些细节(如更详细的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:

隐藏的表单变量MAX_FILE_SIZE告诉了浏览器最大允许上传的文件大小。与很多客户端限制相同,这一限制很容易被攻击者绕开,但它可以为合法用户提供向导。在服务器上进行该限制才是可靠的。
  PHP的配置变量中,upload_max_filesize控制最大允许上传的文件大小。同时post_max_size(POST表单的最大提交数据的大小)也能潜在地进行控制,因为文件是通过表单数据进行上传的。
  接收程序upload.php显示了超级全局数组$_FILES的内容:
CODE:

为了理解上传的过程,我们使用一个名为author.txt的文件进行测试,下面是它的内容:
CODE:
  Chris Shiflett
  http://shiflett.org/
当你上传该文件到upload.php程序时,你可以在浏览器中看到类似下面的输出:
CODE:

虽然从上面可以看出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:

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

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

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

程序向其他访问该页面的用户显示评论。例如,类似下面的代码段可能被用来输出一个评论($comment)及与之对应的发表人($name):
CODE:

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

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

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

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

一个攻击者会首先使用你的应用以收集一些基本信息。例如,攻击者首先访问表单并发现两个表单元素item及quantity,他也同时知道了item的值会是铅笔或是钢笔。

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

攻击者会首先使用这个表单来观察它的动作。例如,在购买了一支铅笔后,攻击者知道了在购买成功后会出现感谢信息。注意到这一点后,攻击者会尝试通过访问下面的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:

通过这些简单的修改,一个跨站请求伪造攻击就必须包括一个合法的验证码以完全模仿表单提交。由于验证码的保存在用户的session中的,攻击者必须对每个受害者使用不同的验证码。这样就有效的限制了对一个用户的任何攻击,它要求攻击者获取另外一个用户的合法验证码。使用你自己的验证码来伪造另外一个用户的请求是无效的。

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

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

通过在你的表单中包括验证码,你事实上已经消除了跨站请求伪造攻击的风险。可以在任何需要执行操作的任何表单中使用这个流程。
  尽管我使用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来判定的方式又是众所周知的。
2.7. HTTP请求欺骗
  一个比欺骗表单更高级和复杂的攻击方式是HTTP请求欺骗。这给了攻击者完全的控制权与灵活性,它进一步证明了不能盲目信任用户提交的任何数据。

  为了演示这是如何进行的,请看下面位于http://example.org/form.php的表单:
CODE:

如果用户选择了Red并点击了Select按钮后,浏览器会发出下面的HTTP请求:
CODE:
  POST /process.php HTTP/1.1
  Host: example.org
  User-Agent: Mozilla/5.0 (X11; U; Linux i686)
  Referer: http://example.org/form.php
  Content-Type: application/x-www-form-urlencoded
  Content-Length: 9

  color=red
.
  看到大多数浏览器会包含一个来源的URL值,你可能会试图使用$_SERVER[‘HTTP_REFERER’]变量去防止欺骗。确实,这可以用于对付利用标准浏览器发起的攻击,但攻击者是不会被这个小麻烦给挡住的。通过编辑HTTP请求的原始信息,攻击者可以完全控制HTTP头部的值,GET和POST的数据,以及所有在HTTP请求的内容。
  攻击者如何更改原始的HTTP请求?过程非常简单。通过在大多数系统平台上都提供的Telnet实用程序,你就可以通过连接网站服务器的侦听端口(典型的端口为80)来与Web服务器直接通信。下面就是使用这个技巧请求http://example.org/页面的例子:
CODE:
$ telnet example.org 80
  Trying 192.0.34.166…
  Connected to example.org (192.0.34.166).
  Escape character is ‘^]’.
  GET / HTTP/1.1
  Host: example.org

  HTTP/1.1 200 OK
  Date: Sat, 21 May 2005 12:34:56 GMT
  Server: Apache/1.3.31 (Unix)
  Accept-Ranges: bytes
  Content-Length: 410
  Connection: close
  Content-Type: text/html

  <html>
  <head>
  <title>Example Web Page</title>
  </head>
  <body>
  <p>You have reached this web page by typing &quot;example.com&quot;,
  &quot;example.net&quot;, or &quot;example.org&quot; into your web browser.</p>
  <p>These domain names are reserved for use in documentation and are not
  available for registration. See
  <a href=”http://www.rfc-editor.org/rfc/rfc2606.txt”>RFC 2606</a>, Section
  3.</p>
  </body>
  </html>

  Connection closed by foreign host.
  $
上例中所显示的请求是符合HTTP/1.1规范的最简单的请求,这是因为Host信息是头部信息中所必须有的。一旦你输入了表示请求结束的连续两个换行符,整个HTML的回应即显示在屏幕上。
  Telnet实用程序不是与Web服务器直接通信的唯一方法,但它常常是最方便的。可是如果你用PHP编码同样的请求,你可以就可以实现自动操作了。前面的请求可以用下面的PHP代码实现:
CODE:

当然,还有很多方法去达到上面的目的,但其要点是HTTP是一个广为人知的标准协议,稍有经验的攻击者都会对它非常熟悉,并且对常见的安全漏洞的攻击方法也很熟悉。
  相对于欺骗表单,欺骗HTTP请求的做法并不多,对它不应该关注。我讲述这些技巧的原因是为了更好的演示一个攻击者在向你的应用输入恶意信息时是如何地方便。这再次强调了过滤输入的重要性和HTTP请求提供的任何信息都是不可信的这个事实。

评论关闭

return top