1. 起因

老项目里一段权限校验逻辑用 == 比较用户角色 id,结果测试同学传了个奇怪的字符串进去,校验直接被绕过了。查出来是 PHP 的弱类型比较在作怪,正好把这些年踩过的几个 == 相关的坑整理一下。

2. 字符串和数字比较,字符串会被隐式转换成数字

1
2
3
4
var_dump(0 == "abc");   // PHP 8 之前: true,PHP 8 之后: false
var_dump(0 == ""); // false(PHP 8 起)
var_dump("1" == "01"); // true,两边都被当成数字比较
var_dump("10" == "1e1"); // true,1e1 是科学计数法的 10

PHP 8 之前,数字和非数字字符串比较时,会把字符串转成 0 再比较,所以 0 == "abc" 在 PHP 7 里是 true,这也是当年著名的”PHP 类型比较魔术”系列坑的源头之一——如果一个字段本来存的是哈希字符串,拿去跟 0 或者 false== 比较,很容易被意外命中。PHP 8 把这条规则改了,改成如果字符串不是数字格式,就把数字转成字符串再比较,0 == "abc" 变成了 false。这是一个版本升级时容易漏掉的行为变化,如果项目里有依赖旧规则的校验逻辑,跨版本升级要格外注意测试这一块。

3. 权限校验里最容易踩的一种

1
2
3
4
5
6
7
8
function checkRole($userRole) {
if ($userRole == 0) { // 想判断"游客"角色(假设游客的 role id 是 0)
return 'guest';
}
return 'member';
}

checkRole('admin_backup'); // 在 PHP 7 里返回 'guest',因为 'admin_backup' == 0 是 true

这就是开头提到的那个真实案例的简化版。角色字段本来设计成整数,但接口没有做强校验,前端传了个非预期的字符串进来,== 比较把它当成 0,直接判定成游客权限——如果这条分支对应的是”降级到最低权限”还好,但如果反过来是”命中了某个高权限分支”,就是安全问题了。

改法很直接,能用 === 的地方全部用 ===,涉及类型不确定的输入,先做显式类型转换或者用 filter_var 做校验:

1
2
3
if ($userRole === 0) {
return 'guest';
}

4. in_array 默认也是弱类型比较

1
2
3
$allowedIds = [1, 2, 3];
var_dump(in_array("abc", $allowedIds)); // PHP 7: 可能为 true,取决于版本
var_dump(in_array("1abc", $allowedIds)); // 同样容易出现意外匹配

in_arrayarray_search 默认都是用 == 做比较,跟单独写 == 一样的坑会在这里重现。这两个函数都有第三个参数 strict,传 true 就会改用 ===

1
in_array($value, $allowedIds, true);

写涉及权限、金额、id 匹配这类校验逻辑的时候,in_array 后面不加 true 基本等于埋雷,这个已经变成我写代码时的条件反射,看到 in_array 就要确认第三个参数有没有传。

5. switch 语句同样是弱类型比较

1
2
3
4
5
6
7
8
9
10
$input = "0";

switch ($input) {
case 0:
echo "matched zero"; // 会命中这里,因为 switch 内部用的是 ==
break;
case "0":
echo "matched string zero";
break;
}

switch 的 case 匹配用的也是 ==,如果 case 列表里同时有数字和字符串,容易匹配到不是你以为的那个分支。PHP 8.0 之后可以用 match 表达式替代,match 内部是严格比较(===),能从语法层面直接规避这类问题。

6. 小结

弱类型比较是 PHP 语言设计上的历史包袱,短期内不会消失,能做的就是养成习惯:涉及权限、金额、身份校验这类逻辑,一律用 ===,用到 in_array/array_search 记得带上 strict 参数,能升级到 PHP 8 的话优先考虑用 match 替代容易踩坑的 switch