本文内容参考自《PHP安全之道》。
主要包括开启register_globals后的全局变量覆盖,可变变量名引起的动态变量覆盖,函数extra()、import_request_variables() 、parse_str()变量覆盖。
全局($GLOBALS)变量覆盖
当php的配置中register_globals全局变量设置开启时, 会自动把url/cookie中传递过去的值注册为全局变量, 这是一个非常危险的设置, 需要关闭。
从PHP4.2开始已经将 register_globals 的默认值从 on 改为 off,从PHP 5.3.0 起废弃并自 PHP 5.4.0 起移除。
错误使用 register_globals = on 的例子:
// 当用户合法的时候,赋值 $authorized = true
if (authenticated_user()) {
$authorized = true;
}
// 由于并没有事先把 $authorized 初始化为 false,
// 当 register_globals 打开时,可能通过GET auth.php?authorized=1 来定义该变量值
// 所以任何人都可以绕过身份验证
if ($authorized) {
include "/highly/sensitive/data.php";
}
当 register_globals = on 的时候,上面的代码就会有危险了。如果在上面的代码执行之前加入 $authorized = false 的话,无论 register_globals 是 on 还是 off 都可以,因为用户状态被初始化为未经认证。
更多信息参考手册: 使用 Register Globals
动态变量覆盖
动态变量(或者叫可变变量), 就是一个变量的变量名可以动态的设置和使用,一个可变变量获取了一个普通变量的值作为这个可变变量的变量名。
$Bar = 'a';
$Foo = 'Bar';
$World = 'Foo';
$Hello = 'World';
$a = 'Hello';
echo $a; //输出 Hello
echo $$a; //相当于 echo $Hello, 输出 World
echo $$a; //相当于 echo $World, 输出 Foo
echo $$$a; //相当于 echo $Foo, 输出 Bar
echo $$$$a; //相当于 echo $Bar, 输出 a
echo $$$$$a; //相当于 echo $a, 输出 Hello
echo $$$$$$a; //相当于 echo $Helo, 输出 World
//... 你还可以添加更多的$符号 ...//
下面看一个使用不当造成安全隐患的例子:
foreach($_POST AS $key=>$value){
$$key = $value; //这里造成了动态变量覆盖
}
if(authenticated_user()){ //认证用户是否登录
$authorized = true;
}
当用户提交的参数中包含authorized=true时, 在执行"认证用户是否登录"之前$authorized的值已经被设置为true,下面的认证已经没有作用了已经越过了权限验证。
为了避免全局变量覆盖的发生, 应尽量不使用动态变量接收客户端参数。上面的代码可以修改为:
$user_name = $_POST['user_name'];
$password = $_POST['password'];
$authorized = false;//变量初始化
if(authenticated_user($user_name, $password)){ //认证用户是否登录
$authorized = true;
}
函数 extract() 变量覆盖
extract — 从数组中将变量导入到当前的符号表。
extract( array &$array[, int $flags = EXTR_OVERWRITE[, string $prefix = NULL]] ) : int
该函数检查每个键名看是否可以作为一个合法的变量名,同时也检查和符号表中已有的变量名的冲突。
extract($_REQUEST); //使用extract造成变量覆盖
if(authenticated_user()){ //认证用户是否登录
$authorized = true;
}
上面的例子发生了与动态变量覆盖同样的bug: 当用户提交的参数中包含authorized=true时, 在执行"认证用户是否登录"之前$authorized的值已经被设置为true,下面的认证已经没有作用了已经越过了权限验证。
为了避免全局变量覆盖的发生, 应尽量不使用extract函数接收客户端参数。上面的代码可以修改为:
$user_name = $_POST['user_name'];
$password = $_POST['password'];
$authorized = false;//变量初始化
if(authenticated_user($user_name, $password)){ //认证用户是否登录
$authorized = true;
}
函数 import_request_variables() 变量覆盖
import_request_variables — 将 GET/POST/Cookie 变量导入到全局作用域中。
该函数只适用于PHP 4.1.0 ~ PHP 5.4.0, 其他版本中不存在该函数。
如果在配置中禁用了register_globals但是又希望导入一些全局变量,可能会用到这个函数。
造成bug的过程和修正方法, 基本同前面的一致。
函数 parse_str() 变量覆盖
parse_str — 将字符串解析成多个变量
parse_str( string $encoded_string[, array &$result] ) : void
如果 encoded_string 是 URL 传递入的查询字符串(query string),则将它解析为变量并设置到当前作用域(如果提供了 result 则会设置到该数组里 )。
$str = 'first=value&arr[]=foo_bar&arr[]=baz';
//第一种情况: 指定输出变量
parse_str($str, $output);
echo $output['first'].PHP_EOL; // 输出: value
echo $output['arr'][0].PHP_EOL; // 输出: foo_bar
echo $output['arr'][1].PHP_EOL; // 输出: baz
//第二种情况: 不指定输出变量()
parse_str($str);
echo $first.PHP_EOL; // 输出: value
echo $arr[0].PHP_EOL; // 输出: foo_bar
echo $arr[1].PHP_EOL; // 输出: baz
在不指定输出变量的情况下, 很容易出现变量覆盖, 造成系统权限限制失效。比如:
parse_str($GLOBALS['HTTP_RAW_POST_DATA']); //获取post中的变量造成变量覆盖
if(authenticated_user()){ //认证用户是否登录
$authorized = true;
}
当用户提交的参数中包含authorize=true时, 用户认证失效。上面的代码可以修改为:
parse_str($GLOBALS['HTTP_RAW_POST_DATA'], $output); //获取post中的变量造成变量覆盖
$authorized = false;
if(authenticated_user($user_name, $password)){ //认证用户是否登录
$authorized = true;
}
极度不建议 在没有 result 参数的情况下使用此函数。
对于第二个参数(输出变量), 在且仅在PHP 7.2中会报警("PHP Deprecated: parse_str(): Calling parse_str() without the result argument is deprecated"), 在其他版本(7.0,7.3,7.4)又不报错。