3.1 弱数据类型安全

由于PHP的弱数据类型特性,造就了PHP的易学和易用。PHP在使用双等号(==)判断的时候,不会严格检验传入的变量类型,同时在执行过程中可以将变量自由地进行转换类型。由于弱数据类型的特点,在使用双等号和一些函数时,会造成一定的安全隐患。
在下面的代码中,当用户输入type=0时,会直接进入支付逻辑。这不是研发人员希望看到的结果。(不会)

  1. <?php
  2. $type = $_GET['type'];
  3. if ($type=='pay'){
  4. echo "Pay";
  5. }else{
  6. echo "Other";
  7. }
  8. $type = (int)$_GET['type'];
  9. var_dump($type);
  10. if ($type=='pay'){
  11. echo "Pay";
  12. }else{
  13. echo "Other";
  14. }

建议使用三个等号(===)来判断变量值与类型是否完全相等。下面经过修改后的代码可以解决这个问题,防止用户传入type = 0时执行支付逻辑。
使用PHP进行判断的时候,为了避免安全漏洞,在使用弱类型机制的时候需要特别留意。下面代码是一些弱类型判断示例。

  1. <?php
  2. var_dump(false == 0);
  3. var_dump(false == '');
  4. var_dump(false == '0');
  5. var_dump(0 == '0');
  6. var_dump(0 == '0xxx');
  7. var_dump(0 == 'xxx');

具体可参考PHP官方文档的松散比较表。
弱数据类型在项目研发过程中,主要表现在Hash比较、bool比较、数字转换比较、switch比较、数组比较等几种比较方式常常被忽视。

3.1.1 Hash比较缺陷

研发人员在对比Hash[插图]字符串的时候常常用到等于(==)、不等于(!=)进行比较。如果Hash值以0e开头,后面都是数字,当与数字进行比较时,就会被解析成0×10n,会被判断与0相等,使攻击者可以绕过某些系统逻辑。

  1. <?php
  2. var_dump('0e123456789'==0);
  3. var_dump('0e123456789'=='0');
  4. var_dump('0e1234abcde'=='0');

当密码经过散列计算后可能会以0e开头。下面示例在进行密码判断时可以绕过登录逻辑。

  1. <?php
  2. //240610708 的hash值就是 0e + 数字的,假设用户输入了314282422居然登陆成功了
  3. $password = "0e462097431906509019562988736854";
  4. $userpwd = $_GET['userpwd'];
  5. if(md5($userpwd ) == $password) {
  6. echo "login success";
  7. }else{
  8. echo "login failed";
  9. }

以下是收集的能hash成 0e 开头的一些字符串

  1. 240610708
  2. 0e462097431906509019562988736854
  3. 314282422
  4. 0e990995504821699494520356953734
  5. 571579406
  6. 0e972379832854295224118025748221
  7. 903251147
  8. 0e174510503823932942361353209384
  9. 1110242161
  10. 0e435874558488625891324861198103
  11. 1320830526
  12. 0e912095958985483346995414060832
  13. 1586264293
  14. 0e622743671155995737639662718498
  15. 2302756269
  16. 0e250566888497473798724426794462
  17. 2427435592
  18. 0e067696952328669732475498472343
  19. 2653531602
  20. 0e877487522341544758028810610885
  21. 3293867441
  22. 0e471001201303602543921144570260
  23. 3295421201
  24. 0e703870333002232681239618856220
  25. 3465814713
  26. 0e258631645650999664521705537122
  27. 3524854780
  28. 0e507419062489887827087815735195
  29. 3908336290
  30. 0e807624498959190415881248245271
  31. 4011627063
  32. 0e485805687034439905938362701775
  33. 4775635065
  34. 0e998212089946640967599450361168
  35. 4790555361
  36. 0e643442214660994430134492464512
  37. 5432453531
  38. 0e512318699085881630861890526097
  39. 5579679820
  40. 0e877622011730221803461740184915
  41. 5585393579
  42. 0e664357355382305805992765337023
  43. 6376552501
  44. 0e165886706997482187870215578015
  45. 7124129977
  46. 0e500007361044747804682122060876
  47. 7197546197
  48. 0e915188576072469101457315675502
  49. 7656486157
  50. 0e451569119711843337267091732412
  51. QLTHNDT
  52. 0e405967825401955372549139051580
  53. QNKCDZO
  54. 0e830400451993494058024219903391
  55. EEIZDOI
  56. 0e782601363539291779881938479162
  57. TUFEPMC
  58. 0e839407194569345277863905212547
  59. UTIPEZQ
  60. 0e382098788231234954670291303879
  61. UYXFLOI
  62. 0e552539585246568817348686838809
  63. IHKFRNS
  64. 0e256160682445802696926137988570
  65. PJNPDWY
  66. 0e291529052894702774557631701704
  67. ABJIHVY
  68. 0e755264355178451322893275696586
  69. DQWRASX
  70. 0e742373665639232907775599582643
  71. DYAXWCA
  72. 0e424759758842488633464374063001
  73. GEGHBXL
  74. 0e248776895502908863709684713578
  75. GGHMVOE
  76. 0e362766013028313274586933780773
  77. GZECLQZ
  78. 0e537612333747236407713628225676
  79. NWWKITQ
  80. 0e763082070976038347657360817689
  81. NOOPCJF
  82. 0e818888003657176127862245791911
  83. MAUXXQC
  84. 0e478478466848439040434801845361
  85. MMHUWUV
  86. 0e701732711630150438129209816536

使用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");
    }

image.png
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");
    }

image.png
比较容易出现问题的做法就是将数据系列化后放入了浏览器的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");
    }

image.png
unserialize示例同理。

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

image.png
在使用变量时要先校验所传入的数据类型是否符合预期,如果超出预期,应该终止系统逻辑执行,避免浮点数在转换成整数时发生意外情况。下面是修复后的代码。

<?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)判断传入的变量是否为整数。如果不是整数,则终止程序的执行。
image.png

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";
    }

image.png
在进入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));

image.png
建议在使用时将$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));

image.png

3.2 PHP代码执行漏洞

PHP提供代码执行(Code Execution)类函数主要是为了方便研发人员处理各类数据,然而当研发人员不能合理使用这类函数时或使用时未考虑安全风险,则很容易被攻击者利用执行远程恶意的PHP代码,威胁到系统的安全。
PHP代码里包含eval()、assert()、preg_repace()、create_function()等能够执行代码的函数,且没有对用户输入的参数进行过滤,会造成代码执行漏洞,可导致攻击者在服务器端任意执行代码,进而控制整个Web服务器。