邪恶八进制信息安全团队技术讨论组's Archiver

独孤依人 2007-4-28 10:47

[转载]高级 PHP 安全(翻译)—— 上半部分

原始出处:[url]http://www.dualface.com/blog[/url]
信息来源:邪恶八进制信息安全团队([url]www.eviloctal.com[/url])
今天从硬盘里面翻出了一个 PDF: Advanced PHP Security,看了一遍,发现里面讲述的内
容非常有价值。因此尝试将主要部分翻译了一下。

其中加入了一些自己的看法和理解,因此和原文有一定差别。

原文本地下载:[url]http://www.dualface.com/blog/downloads/phpworks_security.rar[/url]

输入验证
必须接受的一个关键概念就是:用户输入总是不可靠、不可信任的!

* 在客户端/服务端之间传输时丢失部分数据
* 在处理过程中被破坏
* 用户以未预期的方式修改了数据
* 有意的攻击,尝试获得未经授权的防火或者搞垮应用程序

这就是为什么用户输入的数据在使用前必须进行验证的根本原因。

访问输入数据
在 PHP 中,有一系列的预定义全局变量,提供给开发者访问输入数据的方便途径:

* $_GET - 来自 GET 请求的数据
* $_POST - 来自 POST 请求的数据
* $_COOKIE - cookie 信息
* $_FILES - 上传的文件数据
* $_SERVER - 服务器数据
* $_ENV - 环境变量
* $_REQUEST - GET/POST/COOKIE 的组合数据

Register Globals
许多 PHP 应用程序的漏洞都源自 Register Globals 功能。

* 任何输入参数都将转换为变量:
?foo=bar 将转换为 $foo = “bar”;

* 没有办法确定数据到底来自哪个输入源
根据 php.ini 的设置,COOKIE 的同名数据可能会覆盖 GET 数据

* 通过用户输入可以“注入”未初始化的变量


if (authenticated_user()) {
$authorized = true;
}
if ($authorized) {
include '/highly/sensitive/data.php';
}


上述代码中,如果 authenticated_user() 函数不返回 true,则随后使用的 $authorized
是一个未初始化的变量。因此攻击者可以通过简单的传递 GET 变量来注入 $authorized 变
量,从而绕开安全措施。
例如:[url]http://example.com/script.php?authorized=1[/url]

说明:
当开启了 register globals 功能后,GET 参数会自动转换为变量,因此 ?authorized=1
就会在脚本执行之前生成 $authorized 变量,并设置为 1。从而不管随后的
authenticated_user() 函数返回结果如后,if ($authorized) 条件判断都将为 true。

Register Globals 问题的解决方法
在 php.ini 中禁止 register_globals。从 PHP 4.2.0 版本开始,默认就禁用了该功能,
但许多虚拟主机提供商为了保持对早期应用程序的兼容性,仍然打开来该设置。

设置 error_reporting 为 E_ALL,从而显示所有错误信息。这样当应用程序使用未初始化
变量时,将显示一个警告信息。

使用更严格类型判断条件。因为所有输入数据都是字符串,因此可以在进行判断时,使用
=== 运算符代替 ==。例如: if ($authorized === TRUE)

但是即便这样,register_globals 仍然会带来其他问题。例如下面的代码:


$var[] = “123”;
foreach ($var as $entry) {
make_admin($entry);
}


通过 script.php?var[]=1&var[]=2 就能在 $var 数组中注入两个值。不幸的是在
PHP 里面没有办法检查到这种情况。

$_REQUEST
$_REQUEST 合并了来自多个不同输入源的数据。就像 register_globals,$_REQUEST 也有
变量注入和冲突的危险。在合并多个输入源的数据时,根据 php.ini 的 variables_order
设置不同,不同输入源的优先级也不同。

$_SERVER
尽管 $_SERVER 是由 Web 服务器提供的,但其中的数据仍然是不可信任的:

* 用户可以通过 header 注入数据,例如 Host: <script> …

* 一些数据是根据用户输入构造的,例如 REQUEST_URI、PATH_INFO、QUERY_STRING

* 可能包含伪造的信息,例如通过匿名代理访问服务器,从提供虚假的 IP 地址

特别提示:通过代理访问服务器不等同于伪造 IP 地址。

数值变量验证
所有用户输入数据(GET/POST/COOKIE等)都是字符串类型。使用字符串类型的数值数据,
不但效率低下,而且带有危险。

* 使用类型转换是确保输入数据为数值类型的最简单、最有效的手段


// integer validation
if (!empty($_GET[&#39;id&#39;])) {
$id = (int) $_GET[&#39;id&#39;];
} else
$id = 0;// floating point number validation
if (!empty($_GET[&#39;price&#39;])) {
$price = (float) $_GET[&#39;price&#39;];
} else
$price = 0;
验证字符串
PHP 中带有的 ctype 扩展提供了一些快速验证字符串数据的方法。


if (!ctype_alnum($_GET[&#39;login&#39;])) {
echo "只能包含 A-Za-z0-9.";
}
if (!ctype_alpha($_GET[&#39;captcha&#39;])) {
echo "只能包含 A-Za-z.";
}
if (!ctype_xdigit($_GET[&#39;color&#39;])) {
echo "只能包含16进制数据";
}


路径验证
PHP 应用程序经常都通过输入参数来指定需要打开的文件。这种功能特别需要验证输入,以
防止任意的文件访问。



当使用 [url]http://example.com/script.php?path=../../etc/passwd[/url] 访问该脚本时,实际上
就读取了 /etc/passwd 文件的内容。

为此,可以使用 PHP 提供的 basename() 函数。该函数返回一个路径中最后一部分,通常
是文件名部分。



上述改良过的代码将可以有效避免攻击者对任意文件的访问。

(备注:事实上,我个人从来不依赖根据用户提交的路径来访问文件)

更好的路径验证
更好的解决方案是使用特别的编码来对用户隐藏真正的文件名,并通过一个white-list(白
名单)来验证用户输入的文件名是否有效。


// make white-list of templates
$tmpl = array();
foreach(glob("templates/*.tmpl") as $v) {
$tmpl[md5($v)] = $v;
}if (isset($tmpl[$_GET[&#39;path&#39;]]))
$fp = fopen($tmpl[$_GET[&#39;path&#39;]], "r");
上述代码首先根据指定目录下所有文件的文件名生成一系列 md5 码,然后通过检查用户输
入的 md5 码是否存在即可确定是否能够访问文件。

现在,用户将使用下面的参数访问脚本:
[url]http://example.com/script.php?path=57fb06d7[/url]…

magic_quotes_gpc
PHP 尝试通过对用户输入数据进行自动转义来保护你免受攻击。PHP 将会把用户输入数据中
的 ‘,”,\, 转义。但这么做其实弊大于利:

* 降低了数据处理速度
我们可以使用类型转换验证数值数据;
每一个输入数据都需要两倍的内存。

* 并不总是可用
一些服务器会通过修改 php.ini 禁用该功能。

* 一般化的解决
其他特殊字符也可能需要转义。

正常化 Magic Quotes 转义后的数据

if (get_magic_quotes_gpc()) {
$in = array(&$_GET, &$_POST, &$_COOKIE);
while (list($k,$v) = each($in)) {
foreach ($v as $key => $val) {
if (!is_array($val)) {
$in[$k][$key] = stripslashes($val);
continue;
}
$in[] =& $in[$k][$key];
}
}
unset($in);
}


上述代码能够确保不管 PHP 是否转义了数据,应用程序随后使用的数据都是未转义的。



页: [1]
© 1999-2008 EvilOctal Security Team