3.1 弱数据类型安全
由于PHP的弱数据类型特性,造就了PHP的易学和易用。PHP在使用双等号(==)判断的时候,不会严格检验传入的变量类型,同时在执行过程中可以将变量自由地进行转换类型。由于弱数据类型的特点,在使用双等号和一些函数时,会造成一定的安全隐患。
在下面的代码中,当用户输入type=0时,会直接进入支付逻辑。这不是研发人员希望看到的结果。(不会)
<?php$type = $_GET['type'];if ($type=='pay'){echo "Pay";}else{echo "Other";}$type = (int)$_GET['type'];var_dump($type);if ($type=='pay'){echo "Pay";}else{echo "Other";}
建议使用三个等号(===)来判断变量值与类型是否完全相等。下面经过修改后的代码可以解决这个问题,防止用户传入type = 0时执行支付逻辑。
使用PHP进行判断的时候,为了避免安全漏洞,在使用弱类型机制的时候需要特别留意。下面代码是一些弱类型判断示例。
<?phpvar_dump(false == 0);var_dump(false == '');var_dump(false == '0');var_dump(0 == '0');var_dump(0 == '0xxx');var_dump(0 == 'xxx');
具体可参考PHP官方文档的松散比较表。
弱数据类型在项目研发过程中,主要表现在Hash比较、bool比较、数字转换比较、switch比较、数组比较等几种比较方式常常被忽视。
3.1.1 Hash比较缺陷
研发人员在对比Hash[插图]字符串的时候常常用到等于(==)、不等于(!=)进行比较。如果Hash值以0e开头,后面都是数字,当与数字进行比较时,就会被解析成0×10n,会被判断与0相等,使攻击者可以绕过某些系统逻辑。
<?phpvar_dump('0e123456789'==0);var_dump('0e123456789'=='0');var_dump('0e1234abcde'=='0');
当密码经过散列计算后可能会以0e开头。下面示例在进行密码判断时可以绕过登录逻辑。
<?php//240610708 的hash值就是 0e + 数字的,假设用户输入了314282422居然登陆成功了$password = "0e462097431906509019562988736854";$userpwd = $_GET['userpwd'];if(md5($userpwd ) == $password) {echo "login success";}else{echo "login failed";}
以下是收集的能hash成 0e 开头的一些字符串
2406107080e4620974319065090195629887368543142824220e9909955048216994945203569537345715794060e9723798328542952241180257482219032511470e17451050382393294236135320938411102421610e43587455848862589132486119810313208305260e91209595898548334699541406083215862642930e62274367115599573763966271849823027562690e25056688849747379872442679446224274355920e06769695232866973247549847234326535316020e87748752234154475802881061088532938674410e47100120130360254392114457026032954212010e70387033300223268123961885622034658147130e25863164565099966452170553712235248547800e50741906248988782708781573519539083362900e80762449895919041588124824527140116270630e48580568703443990593836270177547756350650e99821208994664096759945036116847905553610e64344221466099443013449246451254324535310e51231869908588163086189052609755796798200e87762201173022180346174018491555853935790e66435735538230580599276533702363765525010e16588670699748218787021557801571241299770e50000736104474780468212206087671975461970e91518857607246910145731567550276564861570e451569119711843337267091732412QLTHNDT0e405967825401955372549139051580QNKCDZO0e830400451993494058024219903391EEIZDOI0e782601363539291779881938479162TUFEPMC0e839407194569345277863905212547UTIPEZQ0e382098788231234954670291303879UYXFLOI0e552539585246568817348686838809IHKFRNS0e256160682445802696926137988570PJNPDWY0e291529052894702774557631701704ABJIHVY0e755264355178451322893275696586DQWRASX0e742373665639232907775599582643DYAXWCA0e424759758842488633464374063001GEGHBXL0e248776895502908863709684713578GGHMVOE0e362766013028313274586933780773GZECLQZ0e537612333747236407713628225676NWWKITQ0e763082070976038347657360817689NOOPCJF0e818888003657176127862245791911MAUXXQC0e478478466848439040434801845361MMHUWUV0e701732711630150438129209816536
使用hash_equals()函数比较Hash值,可以避免对比被恶意绕过。hash_equals()函数要求提供的两个参数必须是相同长度的字符串,如果所提供的字符串长度不同,会立即返回false。上面的代码应修改如下。
<?php
//240610708 的hash值就是 0e + 数字的,假设用户输入了314282422居然登陆成功了
$password = "0e462097431906509019562988736854";
$userpwd = $_GET['userpwd'];
if(hash_equals($password,md5($userpwd)) {
echo "login success";
}else{
echo "login failed";
}
hash_equals()函数在PHP 5.6中得到支持,如果系统版本号低于5.6,建议进行自定义实现该函数,代码如下。
<?php
if(!function_exists('hash_equals')) {
function hash_equals($a, $b) {
if(!is_string($a) || !is_string($b)) {
return false;
}
$len = strlen($a);
if($len !== strlen($b)) {
return false;
}
$status = 0;
for($i = 0; $i<$len; $i++) {
$status |= ord($a[$i]) ^ ord($b[$i]);
}
return $status === 0;
}
}
3.1.2 bool比较缺陷
在使用json_decode()函数或unserialize()函数时,部分结构被解释成bool类型,也会造成缺陷,运行结果超出研发人员的预期。
<?php
$str = '{"user":true,"pass":true}';
$data = json_decode($str,true);
if ($data['user'] == 'root' && $data['pass'] == 'myPass'){
print_r("login success"."\n");
}else{
print_r("login failed"."\n");
}

unserialize示例代码如下。
<?php
$str = 'a:2:{s:4:"user";b:1;s:4:"pass";b:1;}';
$data = unserialize($str);
if ($data['user'] == 'root' && $data['pass'] == 'myPass'){
print_r("login success"."\n");
}else{
print_r("login failed"."\n");
}

比较容易出现问题的做法就是将数据系列化后放入了浏览器的Cookie中,将用户信息保存在Cookie中是一种极其不安全的做法(详见第3.7节)。避免bool比较隐患的做法是,严格判断数据是否相等的时候使用绝对相等——三个等号(===)。代码修改为如下形式。
<?php
$str = '{"user":true,"pass":true}';
$data = json_decode($str,true);
if ($data['user'] === 'root' && $data['pass'] === 'myPass'){
print_r("login success"."\n");
}else{
print_r("login failed"."\n");
}
3.1.3 数字转换比较缺陷
当赋值给PHP变量的整型超过PHP的最大值PHP_INT_MAX时,PHP将无法计算出正确结果,攻击者可能会利用其跳过某些校验逻辑,如密码校验(无须输入正确的密码就可以直接登录用户的账号)、账号充值校验(充值很小的金额就可以进行巨额资金入账)等。下面代码示例中$a、$b、$aa、$bb均超出了PHP的最大值,所以运算结果超出了预期。
<?php
$a = 92233720368547758079223372036854775807;
$b = 92233720368547758079223372036854775819;
$aa = '92233720368547758079223372036854775807';
$bb = '92233720368547758079223372036854775819';
var_dump($a===$b);//bool(true)
var_dump($a%100);//int(0)
var_dump($b%100);//int(0)
var_dump($aa===$bb);//bool(false)
var_dump($aa%100);//int(7)
var_dump($bb%100);//int(7)
在实际的业务逻辑中(如充值金额、订单数量),一定要对最大值进行限制,避免数据越界而导致错误的执行结果。下面是一段商品购买的示例代码,其中对传入的价格和购买数量进行了范围校验,可避免数据越界产生错误的结果。
当赋值给PHP变量超长浮点数时,PHP的结果也将出现错误。在下面的代码示例中,当uid=0.99999999999999999时,代码逻辑会正常进入if语句,查询出uid=0的用户信息。以此类推,1.99999999999999999将会跳入$uid==”2”的判断中。
在数据库中新建表,插入数据
create table users;
insert into users values(1,'zhangsan');
insert into users values(2,'lisi');
<?php
$servername = "localhost";
$username = "root";
$password = "root";
// 创建连接
$conn = mysql_connect($servername, $username, $password);
// 检测连接
if ($conn->connect_error) {
die("连接失败: " . $conn->connect_error);
}
echo "连接成功";
$uid = $_GET['uid'];
if ($uid == "1"){
$uid = intval($uid);
$query = "SELECT * FROM users where uid=$uid;";
}
$result = mysql_query($query,$conn) or die(mysql_error());
// print_r($result);
print_r(mysql_fetch_row($result));

在使用变量时要先校验所传入的数据类型是否符合预期,如果超出预期,应该终止系统逻辑执行,避免浮点数在转换成整数时发生意外情况。下面是修复后的代码。
<?php
$servername = "localhost";
$username = "root";
$password = "root";
// 创建连接
$conn = mysql_connect($servername, $username, $password);
// 检测连接
if ($conn->connect_error) {
die("连接失败: " . $conn->connect_error);
}
echo "连接成功";
$uid = $_GET['uid'];
if(!is_int($uid)){
die("传入的uid数据类型错误,系统已终止");
}
if ($uid == "1"){
$uid = intval($uid);
$query = "SELECT * FROM users where uid=$uid;";
}
$result = mysql_query($query,$conn) or die(mysql_error());
// print_r($result);
print_r(mysql_fetch_row($result));
在代码中添加is_int($uid)判断传入的变量是否为整数。如果不是整数,则终止程序的执行。
3.1.4 switch比较缺陷
当在switch中使用case判断数字时,switch会将其中的参数转换为int类型进行计算,如以下代码所示。
<?php
$num = "2hacker";
switch ($num){
case 0:
echo "say no hacker";
break;
case 1:
echo "say one hacker";
break;
case 2:
echo "say two hacker";
break;
default:
echo "i dont know";
}

在进入switch逻辑前一定要判断数据的合法性,对不合法的数据要进行及时阻断,防止恶意攻击者越过逻辑,出现逻辑错误。
<?php
$num = "2hacker";
if (!is_numeric($num)){
die("错误的数据类型,禁止访问");
}
switch ($num){
case 0:
echo "say no hacker";
break;
case 1:
echo "say one hacker";
break;
case 2:
echo "say two hacker";
break;
default:
echo "i dont know";
}
3.1.5 数组比较缺陷
当使用in_array()或array_search()函数时,如果$strict参数没有设置为true,则in_array()或array_search()将使用松散比较来判断$needle是否在$haystack中。
bool in_array(mixed $needle,array $haystack[,bool $strict=false]) //strict默认为false
mixed array_search(mixed $needle,array $haystack[,bool $strict=false]) //strict默认为false
记得调整php版本
<?php
$array=[0,1,2,'3'];
var_dump(in_array('abc',$array));
var_dump(array_search('abc',$array));
var_dump(in_array('1bc',$array));
var_dump(array_search('1bc',$array));

建议在使用时将$strict的值设置为true,这样in_array()或array_search()就会严格地比较$needls的类型与$haystack中的类型是否相同,以避免一些安全问题。下面是修复后的代码。
<?php
$array=[0,1,2,'3'];
var_dump(in_array('abc',$array,true));
var_dump(array_search('abc',$array,true));
var_dump(in_array('1bc',$array,true));
var_dump(array_search('1bc',$array,true));
3.2 PHP代码执行漏洞
PHP提供代码执行(Code Execution)类函数主要是为了方便研发人员处理各类数据,然而当研发人员不能合理使用这类函数时或使用时未考虑安全风险,则很容易被攻击者利用执行远程恶意的PHP代码,威胁到系统的安全。
PHP代码里包含eval()、assert()、preg_repace()、create_function()等能够执行代码的函数,且没有对用户输入的参数进行过滤,会造成代码执行漏洞,可导致攻击者在服务器端任意执行代码,进而控制整个Web服务器。

