目录
0x1:什么是安全的Web应用程序
0x2:过滤输入的数据
0x3:转义输出的数据
0x4:Register Globals
0x5:magic_quotes_gpc
0x6:错误信息的报告
0x7:文件的安全
0x8:Session的安全
0x9:虚拟主机
你所开发的Web应用程序,可能是用于以下用途:
(1)用在个人网站展示图片或文章
(2)展示商品来吸引顾客购买
(3)公司内部使用或对外开放浏览
(4)大型的跨国际网站
不管是哪一种,在花费大量时间和精力建立了Web站点之后,你当然会希望运行在站点的Web程序能够正常的运行。最重要的是不会受到黑客的攻击破坏。
在没有任何的硬件或者软件方面的设置的情况下,想要完全地保障你的Web应用程序的安全是不可能的。
但是只要记住两个最基本的验证法则:
(1)过滤输入的数据(filter input)
(2)转义输出的数据(escape output)
使用者输入的数据一定要经过过滤,才能够赋值给变量、数据库、cookie、session内。即使输入的数据是自己设置的,而不是来自别人输入的,也不能相信不会有问题。
要输出到屏幕、数据库和文件内的数据一定要经过转义(escape)。所谓转义就是将特殊字符进行转换,使隐藏在输出数据内的恶意代码被转义而失效。
过滤输入的数据
PHP提供下列超全局变量(super globals),让你轻松地存取使用者输入的数据:
(1)$_GET:从HTTP请求而来
(2)$_POST:从表单数据而来
(3)$_COOKIE:从cookie数据而来
(4)$_FILES:上传的文件数据
(5)$_SERVER:服务器的数据
(6)$_ENV:环境变量
(7)$_REQUEST:GET/POST/COOKIE的结合
过滤输入的数据可以使用常规表达式(regular expression)来验证
例如:验证邮箱地址的合法性:
preg_match("/^.+@.+\..{2,3}$/", $_POST["email"]);
数字数据的过滤:
所用使用GET、POST、COOKIE传递给PHP程序文件的数据,最后一定是字符串的形式。如果需要的是整数但使用字符串来传递,不但没有效率而且危险。
如果你确定输入的数据一定是数字,你可以使用数据类型转换的方式来将输入的数据转换成数字。
例如:stringtoint.php
<?php
//整数
$id=0;
if(!empty($_GET["id"]))
{
$id=(int)$_GET["id"];
}
//浮点数
$price=0;
if(!empty($_GET["price"]))
{
$price=(float)$_GET["price"];
}
?>
另一种方式是使用intval函数来将数据转换为整数,使用floatval函数来将数据转换成浮点数。
例如:val.php
<?php
//整数
$id=0;
if(!empty($_GET["id"]))
{
$id=intval($_GET["id"]);
}
//浮点数
$price=0;
if(!empty($_GET["price"]))
{
$price=floatval($_GET["price"]);
}
?>
使用上述两种方法,就可以确保输入的数据一定是数字。
字符串数据的过滤函数:
PHP提供ctype_系列的函数来验证字符串的内容
(1)ctype_alnum -- Check for alphanumeric character(s)
检测是否是只包含[A-Za-z0-9]
(2)ctype_alpha -- Check for alphabetic character(s)
检测是否是只包含[A-Za-z]
(3)ctype_cntrl -- Check for control character(s)
检查是否是只包含类是“\n\r\t”之类的字 符控制字符
(4)ctype_digit -- Check for numeric character(s)
检查时候是只包含数字字符的字符串(0-9)
(5)ctype_graph -- Check for any printable character(s) except space
检查是否是只包含有可以打印出来的字符(除了空格)的字符串
(6)ctype_lower -- Check for lowercase character(s)
检查是否所有的字符都是英文字母,并且都是小写的
(7)ctype_print -- Check for printable character(s)
检查是否是只包含有可以打印出来的字符的字符串
(8)ctype_punct -- Check for any printable character which is not whitespace or an alphanumeric character
检查是否是只包含非数字/字符/空格的可打印出来的字符
(9)ctype_space -- Check for whitespace character(s)
检查是否是只包含类是“ ”之类的字符和空格
(10)ctype_upper -- Check for uppercase character(s)
检查是否所有的字符都是英文字母,并且都是大写的
(11)ctype_xdigit -- Check for character(s) representing a hexadecimal digit
检查是否是16进制的字符串,只能包括 “0123456789abcdef”
例如:instance.php
<?php
if(!ctype_alnum($_GET["username"]))
{
echo "只允许A-Z,a-z,0-9的字符串";
}
if(!ctype_alpha($_GET["name"]))
{
echo "只允许A-Z,a-z的字符";
}
if(!ctype_xdigit($_GET["rgb"]))
{
echo "只允许16进制数字的字符";
}
?>
例如:backlogin.php
如果用户输入的账户只包含字母或数字,就将输入的数据保存在数组中
<?php
$clear=array();
if(ctype_alnum($_POST["username"]))
{
$clear["username"]=$_POST["username"];
}
?>
HTML与PHP标签的过滤
strip_tags函数可以用来去除输入字符串中的HTML与PHP标签。当使用者输入的字符串中包含HTML标签或者javascript代码时,使用strip_tags函数就可以将这些能够执行的HTML标签或者javascript程序代码过滤掉。
例如:strip.php
<?php
function _INPUT($var_name)
{
if($_SERVER["REQUEST_METHOD"] == "GET")
{
return strip_tags($_GET[$var_name]);
}
if($_SERVER["REQUEST_METHOD"] == "POST")
{
return strip_tags($_POST[$var_name]);
}
}
$username = _INPUT("username");
?>
不管是从GET还是POST方式取得的数据,都先经过strip_tags函数来删除输入字符串中的HTML与PHP标签。
文件路径的过滤
传递给PHP程序文件的文件路径值,通常是用来指定要打开的文件。这个路径值也需要过滤,以避免黑客存取任意的文件。
例如:openfile.php
<?php
$fp=fopen("/home/user/{$_GET['file']}", "r");
?>
黑客可以使用下列URI来发动目录穿越攻击:
openfile?file=../../etc/passwd
PHP提供basename函数来删除文件路径中除了最后的文件名称以外的所有路径字符串。
例如:basename.php
<?php
$_GET["file"] = basename($_GET["file"]);
if(file_exists("/home/user/{$_GET['file']}"))
{
$fp=fopen("/home/user/{$_GET['file']}", "r");
}
?>
更好的方法是隐藏文件的名称不要让使用者知道,并且创建允许文件的名称列表以在程序中存取
例如:filelist.php
<?php
//创建允许文件的名称列表
$file_list=array();
//将允许的文件以.lst的扩展名保存
foreach(glob("*.lst") as $filename)
{
$file_list[md5($filename)]=$filename;
}
//URL中的文件名称存在于允许文件的名称列表内
if(isset($file_list($_GET["file"])))
{
$fp=fopen($file_list[$_GET["file"]], "r");
}
?>
序列化字符串的过滤
许多应用程序会使用GET、POST或者COOKIE方法来传递序列化字符串(serialized data)。序列化字符串是一种PHP的内部格式,用来传递复杂的变量类型,例如数组或者对象。
序列化字符串的格式并没有内部检验的机制,因此几乎任何形态的输入数据都可能会被接受。特别设计的连续字符串有以下作用:
(1)让PHP应用程序崩溃
(2)消耗大量的内存
(3)在某些PHP版本上能引发命令植入攻击
解决的方法如下:
(1)尽量不要依赖使用者可以存取的方法来传递序列化字符串
(2)创建数据的checksum,在将序列化字符串传递给unserialize函数之前先检查checksum是否一致。
例如:checksum.php
<?php
if(md5($_POST["serialize_data"]) == $_SESSION["checksum"])
{
$data = unserialize($_POST["serialize_data"]);
}
else
{
//生成警告信息
trigger_error("可疑的序列化数据", E_USER_ERROR);
}
?>
转义输出的数据
转义(escape)是将特定的字符加上特定的符号。
例如,设置php.ini文件的magic_quotes_gpc=On,就会将输入字符串内的所有"'"、"""、"\",以及NULL字符都加上一个"\"来转义。
如果不是SQL表达式的字符串,你使用htmlspecialchars函数或htmlentities函数来转义。
htmlspecialchars函数会将下列的特殊字符转换成HTML字符吗:
(1)& 转换成 &
(2)当没有设置ENT_NOQUOTES时,双引号转换成 "
(3)当设置ENT_QUOTES时,单引号转换成 '
(4)"<" 转换成 <
(5)">" 转换成 >
htmlentities函数会将所有的特殊字符都转换成HTML字符吗。如果是SQL表达式的字符串,使用mysql_real_escape_string函数来转义。
基本的转义程序:
<?php
$html=array();
$html["username"]=htmlspecialchars($clean["username"], ENT_QUOTES);
$html["username"]=htmlentities($clean["username"], ENT_QUOTES, "utf-8");
echo "<p>访客:{$html["username"]}</p>";
?>
转义SQL表达式的字符串:
<?php
$mysql=array();
$mysql["username"]=mysql_real_escape_string($clean["username"]);
$sql = "select * from member where username = '{$mysql['username']}'";
$result = mysql_query($sql);
?>
使用addslashes函数:
addslashes函数会将字符串中的单引号、双引号、反斜杠以及NULL字符加上反斜杠:
<?php
$mysql=array();
$mysql["username"] = addslashes($clean["username"]);
$sql = "select * from member where username = '{$mysql['username']}'";
$result = mysql_query($sql);
?>
Register Globals
在php.ini文件中设置register_globals=On的时候,就会打开register globals的功能。register globals被认为是PHP应用程序的最主要漏洞,原因如下:
(1)任何输入的参数都会被转换成变量,例如在URI中设置:
?username=milantgh
在PHP程序文件中,$username会被设置为milantgh
$username="milantgh"
(2)无法确定输入数据的来源,有优先权的来源会覆盖GET的数值,例如:cookie
(3)未初始化的变量可能经由使用者输入的数据来植入,例如:over.php
<?php
//使用者经过check_user自定义函数的验证
if(check_user())
{
$authorized = true;
}
if($authorized)
{
include "/home/user/data.php";
}
?>
如果使用者验证失败的话,$authorized变量就不会初始化数据,黑客可以使用GET方法来传递数据给$authorized变量:
over.php?authorized=1 (1 == true)
解决的方法如下:
(1)在php.ini文件中设置register_globals=Off,从PHP 4.2.0开始这就是默认值
(2)在php.ini文件中设置error_reporting=E_ALL,当使用未初始化的变量时就会收到警告信息
(3)由于从GET得到的使用者输入一定是字符串,因此比较数据的类型就可以辨认变量数据的来源。将使用者输入的数据与布尔或整数进行数据类型的比对,永远会失败。例如:convert.php
<?php
//如果使用者经过check_user自定义函数的验证
if(check_user())
{
$authorized = true;
}
if($authorized === true)
{
include "/home/user/data.php";
}
?>
隐藏Register Globals所发生的问题,例如:hidden.php
<?php
$var[] = "123";
foreach($var as $v)
{
echo "\$v=$v"."<br />";
}
?>
黑客可以使用GET方法来传递数据给$var数组:
hidden.php?var[]=1&var[]=2
这会将1和2写入$var数组中,PHP没有提供工具来检测这种赋值的问题。
$_REQUEST变量
$_REQUEST自动变量会从不同的输入来源中合并数据,因此与register globals一样容易遭受输入数据的攻击。$_REQUEST从不同的输入来源读取的顺序是由php.ini文件中的variables_order来决定的:
variables_order = "EGPCS"
EGPCS分别代表的数据输入来源如下:
E:$_ENV环境变量
G:$_GET变量
P:$_POST变量
C:$_COOKIE变量
S:$_SERVER服务器变量
数据的加载顺序是从左到右,新的数据会覆盖旧的数据,例如:
$_GET["id"]=1,$_COOKIE["id"]=2,结果$_COOKIE["id"]的数据会覆盖$_GET["id"]的数据,所以,$_REQUEST["id"]=2。
$_SERVER变量
虽然$_SERVER自动变量是根据服务器所提供的数据而来,但是一样不能完全相信$_SERVER的数据不会有问题:
(1)恶意的使用者可以在HTTP表头中插入javascript程序代码:
Host:<script>alert("Hello milantgh");</script>
(2)有些$_SERVER变量的数值是根据使用者的输入而来,例如:REQUEST_URI、PATH_INFO、QUERY_STRING
(3)可能是使用匿名的代理服务器所伪造的IP地址
magic_quotes_gpc
如果将php.ini文件中的magic_quotes_gpc=On,那么PHP会自动将字符串中的单引号、双引号、反斜杠以及NULL字符都加上一个反斜杠来转义,来保护你的应用程序不会遭受黑客的攻击。
使用magic_quotes_gpc=On有如下的缺点:
(1)使用magic_quotes_gpc会让输入的处理变慢
(2)使用数据类型来转换整数的输入会比较好
(3)每个输入的数据都需要两倍的内存
(4)如果在php.ini文件中被禁止就不能使用
(5)还有其他的字符可能也需要转义
例如:escape.php
<?php
//magic_quotes_gpc为On
if(get_magic_quotes_gpc())
{
$data = array(&$_GET, &$_POST, &$_COOKIE);
while(list($item, $val) == each($data))
{
foreach($val as $key=>$value)
{
if(!is_array($value))
{
$data[$item][$key] = stripslashes($value);
continue;
}
$data[] = &$data[$item][$key];
}
}
unset($data);
}
?>
错误信息的报告
默认情况下PHP会在屏幕上打印所有的错误信息。这些错误信息会让使用者受到惊吓,甚至有时候会显示服务器的信息,例如:文件路径、未初始化的变量、函数参数、账号密码。
但是,关掉错误报告又会让程序无法解决问题
你可以将php.ini文件的display_errors=Off,使错误信息不会被显示在屏幕上:
ini_set("display_errors", FALSE);
你可以将php.ini文件的log_errors=Off,并且将error_log设置为指定的文件名称,将错误信息写入到自定义的文件中:
ini_set("log_errors", TRUE);
ini_set("error_log", "/user/log/error.log");
或者写到系统的log文件中:
ini_set("error_log", "syslog");
文件的安全
许多PHP应用程序需要用到各种公用文件(utility file)或者是配置文件(configuration file),如果这些文件放在Web应用程序的文件夹内,使用者就可以下载或者是查看文件的内容。
因此,为了保障文件的安全,不要将不必要的文件放在Web应用程序的根目录内;使用.htaccess文件来限制文件或者文件夹的存取。
Session的安全
Session是Web应用程序用来跟踪使用者操作的常用工具,尤其是用在各个页面中识别使用者的身份。如果黑客可以存取到作用中的session,那么这个session的使用者的身份就会被黑客所盗用。
Session固定攻击
Session固定攻击是将Session id的数据设置成某个已知的数据。如果成功的话,黑客只要传递这个已知的Session id,就可以冒充目标用户的身份。
攻击的方式最常见的就是让使用者单击黑客设置好的超级链接,在这个超级链接里面植入已知的Session id的数据:
<a href="http://www.milantgh.com/?PHPSESSID=123">XSS脚本系列新书</a>
如果使用者还没有建立Session,那么他的Session id就是123。
要避免使用到黑客指定的Session id,登陆后应该立即调用session_regenerate_id函数来产生一个新的Session id:
<?php
session_start();
//检查登陆的程序代码
//使用者成功登陆
if($login_OK)
{
//创建新的Session id
session_regenerate_id();
}
?>
另一种保护session安全的技术,就是比较浏览器的表头字符串:
<?php
session_start();
$session_code = @md5($_SERVER["HTTP_ACCEPT_CHARSET"].$_SERVER["HTTP_ACCEPT_ENCODING"].$_SERVER["HTTP_ACCEPT_LANGUAGE"].$_SERVER["HTTP_USER_AGENT"]);
if(empty($_SESSION))
{
$_SESSION["session_code"]=$session_code;
}
else if($_SESSION["session_code"] != $session_code)
{
session_destroy();
}
?>
Session的保存
默认PHP的Session是保存在文件内,这个文件放置在一般的temp文件夹中。
这表示在系统上的任何使用者都可以看到作用中的Session,甚至修改Session的内容。
解决的方式就是使用php.ini文件内的Session.save_path选项来另外指定保存Session文件的文件夹。
虚拟主机
大部分的PHP应用程序是在虚拟主机(shared hosting)的环境内执行,所有的使用者共同分享服务器的资源。这表明网站服务器可以读取主机内的所有文件。这就是我们常说的旁注入侵。
PHP的解决方式是使用php.ini文件内的两个选项:
(1)open_basedir:用来限制能够存取文件的文件夹。(这种做法相当有效率,而且也不复杂)
(2)safe_mode:根据程序文件的使用者id或者用户组id来限制文件的存取。(这种做法速度慢,而且复杂,黑客可以轻易绕过)
可预测的临时文件名称
临时文件夹内的已知名称并且可以写入的文件,可以使用symlink函数来建立符号链接:
例如:link.php
<?php
//清除旧的错误
$fp=fopen("/temp/script_errors", "w");
fclose($fp);
?>
黑客可以使用symlink函数来建立符号链接:
<?php
symlink("/etc/passwd", "/temp/script_errors");
?>
解决的方法如下:
(1)不要使用可预测的文件名称,tmpfile函数可以用来建立一个临时文件,tempnam函数可以使用特殊的文件名称来建立文件
(2)如果无法避免要使用已知的文件名称,你可以使用is_link函数来测试文件名称是否是符号链接。如果要打开文件来清除旧的错误,不如直接使用unlink函数来删除整个文件。
隐藏表头的信息
(1)将php.ini文件中的PHP识别表头功能禁止
expose_php=Off
(2)将http.conf文件中的Apache识别表头功能禁止:
ServerSignature=Off
系统异常的监测
下列属于主机的异常行为:
A:不正常断线
B:不正常重新开机
C:多余的网络连接
D:过高的CPU使用率
E:登陆文件丢失
F:文件权限被修改
G:不知来源的隐藏文件