PHP项目安全基础二(编码安全) - Zanealancy博客

PHP项目安全基础二(编码安全)

PHP 是一种弱类型(动态类型)语言,这意味着变量的类型会根据上下文自动转换。这种灵活性虽然提高了开发效率,但也带来了安全风险,尤其是在处理用户输入、身份验证、权限控制等敏感逻辑时。

一、PHP 弱类型带来的主要安全问题

1. 类型自动转换

PHP 在比较操作(尤其是使用 == 而非 ===)时,会进行隐式类型转换,可能导致意外结果。

var_dump("0e12345" == "0e67890"); // true!因为都被当作科学计数法 0 的浮点数
var_dump("123abc" == 123);        // true!字符串被转为整数 123
var_dump("admin" == 0);           // true!非数字字符串转为 0

⚠️ 这在登录验证、密码比对、权限判断中极易被利用。


2. 哈希比较漏洞

许多系统使用 md5()sha1() 生成哈希,并用 == 比较。

攻击示例:

// 假设数据库中存储的密码是 md5('aabg7XSs')
if (md5($_POST['password']) == $stored_hash) {
    login(); // 可能被绕过!
}

修复方式:使用 === 严格比较,或使用 hash_equals()(防时序攻击)。


3. 数组注入

若代码未校验类型,可能引发错误或绕过逻辑。

示例:

// 用户提交:?token[]=any
$token = $_GET['token'];
if ($token == "secret") { // 数组与字符串比较,结果为 false,但可能触发其他逻辑
    grant_access();
}

#在 `in_array()` 中若不启用严格模式:
in_array(0, ['admin', 'user']); // 返回 true!因为 'admin' 被转为 0

修复:使用 in_array($value, $array, true) 启用严格模式。


4. 布尔/空值误判

if ($_GET['admin']) { ... }

如果用户传入 admin=0,PHP 会将 "0" 转为 false,看似安全;但如果传入 ?admin=(空字符串)或 ?admin=false(字符串 "false"),结果仍是 false,“只要存在 admin 参数就授权”,则应检查 isset() 而非值:

// 正确(若意图是“存在即授权”)
if (isset($_GET['admin'])) { ... }

// 更安全:白名单校验
if ($_GET['admin'] === 'true') { ... }

在 PHP 中,bool 比较缺陷(尤其是与 json_decode()unserialize() 相关)是常见的安全风险点。这类问题通常源于:

  • 对反序列化/解码后数据的类型未做严格校验;
  • 使用弱比较(==)处理布尔值;
  • 忽略了 json_decode() 在特定输入下返回 false 的行为。

下面分别从 json_decodeunserialize 两个角度详细说明其与 bool 相关的安全缺陷及防御方法。


4.1 json_decode() 失败时返回 false

当传入非法 JSON 字符串时,json_decode() 返回 false(而非抛出异常),而 false 是一个布尔值。

$data = json_decode($_GET['input']);
if ($data['admin'] == true) {
    grant_admin();
}

攻击者可提交:

?input=invalid_json

$data = false
$data['admin'] 触发 PHP Warning,但表达式结果为 null
null == truefalse,看似安全?

但更危险的是:如果后续逻辑未判断 $data === false,可能误将 false 当作有效数据处理

更隐蔽的攻击(利用 false 参与数组访问):
// 假设代码:
$input = json_decode($_POST['data']);
if ($input && $input->is_admin == true) {
    do_admin_stuff();
}

若攻击者提交非 JSON 数据(如空字符串、"false" 字符串等):

  • json_decode("")NULL
  • json_decode("false")false(布尔值)
  • json_decode("0")0
  • json_decode("null")NULL

⚠️ 注意:json_decode("false") 返回的是 布尔 false,不是字符串!

此时:

$input = false;
var_dump($input->is_admin); // Fatal error: Trying to get property 'is_admin' of non-object

虽然会报错,但如果错误被抑制(如生产环境关闭 display_errors),或代码有容错逻辑,可能被绕过。


4.2 JSON 中的布尔值本身可能被弱比较误判

合法 JSON 可包含布尔值:

{"is_admin": true}

但如果使用弱比较:

if ($obj->is_admin == "1") { ... } // true == "1" → true ✅
if ($obj->is_admin == "true") { ... } // true == "true" → true ❌(因为 "true" 转为 1)

更危险的是:

if ($obj->is_admin == 1) { ... } // 安全
if ($obj->is_admin == "0") { ... } // true == "0" → false,看似没问题

但若字段被篡改为字符串 "true"(非布尔):

{"is_admin": "true"}

$obj->is_admin == true"true" == true1 == 1true!

结论:永远不要用弱比较判断布尔逻辑。


4.3 反序列化返回任意类型,包括 bool

$data = unserialize($_COOKIE['user']);
if ($data['is_admin'] == true) {
    // 危险!
}

攻击者可构造序列化数据:

// 序列化一个布尔 true
echo serialize(true); // b:1;

提交 b:1; 作为输入 → $data = true
$data['is_admin']Warning + null
null == true → false

但更危险的是构造数组:

$payload = serialize(['is_admin' => true]);
// a:1:{s:8:"is_admin";b:1;}

这本身是合法的。但如果代码未校验结构或类型,可能被利用。

4.4 反序列化对象属性为布尔时的弱比较

假设类定义:

class User {
    public $is_admin = false;
}

攻击者无法直接控制 $is_admin 为字符串,但如果存在魔术方法(如 __wakeup)或类型混淆,仍可能出问题。

更大的风险其实是 反序列化漏洞本身(任意对象注入),但这里聚焦在 布尔比较缺陷


**安全修复建议**

✅ 1. 始终校验解码/反序列化是否成功
$data = json_decode($input, true);
if (json_last_error() !== JSON_ERROR_NONE) {
    die('Invalid JSON');
}
// 或
if ($data === null && $input !== 'null') {
    die('Invalid input');
}

对于 unserialize()

$data = @unserialize($input, ['allowed_classes' => false]);
if ($data === false && $input !== 'b:0;') {
    die('Unserialize failed');
}

注意:unserialize(false) 返回 false,但 serialize(false)'b:0;',需特殊处理。


✅ 2. 使用严格类型校验

// 期望 is_admin 是布尔值
if (!isset($data['is_admin']) || !is_bool($data['is_admin'])) {
    die('Invalid type for is_admin');
}

if ($data['is_admin'] === true) {
    // 安全
}

或者强制转换(不推荐,但比弱比较好):

if ((bool)$data['is_admin']) { ... }

但最好配合白名单:

$allowed_values = [true, false];
if (!in_array($data['flag'], $allowed_values, true)) {
    // 第三个参数 true 启用严格模式
}

✅ 3. 避免使用 unserialize() 处理用户输入

  • 改用 json_decode()(更安全)
  • 如必须用 unserialize(),务必:
    • 设置 allowed_classes
    • 验证数据结构
    • 不用于敏感逻辑(如权限)

5. 数字比较转换缺陷

当使用 == 比较两个不同类型的值时,PHP 会尝试将它们转换为同一类型再比较。其中,字符串转数字的规则非常宽松:

  • 如果字符串以数字开头(如 "123abc"),则取前面的数字部分(123);
  • 如果字符串不以数字开头(如 "abc123" 或 "admin"),则转为 0;
  • 科学计数法字符串(如 "0e12345")会被解析为浮点数 0。

PHP 的数字比较转换缺陷(Numeric Comparison Type Juggling Vulnerability)是弱类型语言中最经典、最危险的安全问题之一。它常出现在使用 ==(弱等于)而非 ===(严格等于)进行比较的场景中,尤其在处理用户输入时极易被利用来绕过身份验证、权限控制、支付金额校验等关键逻辑。


5.1 登录/权限绕过(最常见)

// 不安全代码
if ($_POST['user_id'] == "admin") {
    grant_admin();
}

攻击者提交:

user_id=0

"0" == "admin"0 == 0true!

✅ 因为 "admin" 被转为整数 0"0" 也是 0

🔥 实际案例:很多 CTF 题和真实系统漏洞都源于此。


5.2 科学计数法绕过

if ($_GET['token'] == "999") {
    unlock_feature();
}

攻击者提交:

?token=999e0

"999e0" 被解析为浮点数 999.0
999.0 == "999"999 == 999true

更隐蔽的:

?token=1e3   // 等于 1000
?token=0x10  // PHP 7+ 中十六进制字符串不再自动转数字(但旧版本可能)

⚠️ 注意:PHP 7.0+ 已禁止 == 比较时将十六进制字符串(如 "0x10")自动转为数字,但科学计数法仍有效。


5.3 数组 vs 数字

if ($_GET['id'] == 1) {
    show_user();
}

攻击者提交:

?id[]=1

$_GET['id'] 是数组 [1]
[1] == 1true!

因为在 PHP 中:

var_dump([1] == 1);   // true
var_dump([0] == 0);   // true
var_dump([] == 0);    // true

💥 这可能导致 SQL 注入(如果后续拼接数组到 SQL)、逻辑绕过等。


5.4 空值/特殊字符串被转为 0

字符串 转为数字
"" 0
" " 0
"0" 0
"0.0" 0.0
"false" 0
"true" 1
"null" 0
"undefined" 0

示例:

if ($_POST['amount'] == 100) {
    charge_user();
}

攻击者提交:

amount=100.00000000000001  // 可能因浮点精度问题失败
amount=1e2                  // 成功(=100)
amount=0                    // 如果逻辑是“只要不是0就收费”,但写成 ==0 判断,也可能出错

5.5 高危函数组合

以下函数在配合弱比较时风险极高:

函数 风险说明
in_array($needle, $haystack) 默认使用 ==in_array(0, ['admin']) 返回 true
array_search() 同上
switch ($var) 使用 == 比较 case 值
strcmp(), strcasecmp() 若传入非字符串(如数组),返回 NULL,而 NULL == 0

二、安全编码建议

风险点 安全做法
比较操作 使用 ===!== 替代 == / !=
哈希比较 使用 hash_equals($hash1, $hash2)
数组处理 显式检查 is_array(),避免隐式转换
输入验证 使用 filter_var()、类型声明(PHP 7+)、严格模式
配置 设置 declare(strict_types=1);(仅对当前文件生效)
场景 是否安全? 建议
$_GET['id'] == 1 改为 === 或先转 int 再比较
in_array($user, $admins) true 参数
switch($_POST['action']) ⚠️ 确保 action 是预期内的字符串
json_decode() 后直接比较 ⚠️ 先校验类型(is_int, is_string
用户输入用于金额/ID/权限 必须严格类型校验 + 白名单

✅改进

  • 标量类型声明(Scalar Type Declarations):
    function login(string $username, string $password): bool {
      // 强制参数为字符串
    }
  • 严格模式
    declare(strict_types=1);

注意:strict_types=1 只影响当前文件的函数调用和返回值,不影响内部 PHP 函数(如 array_search 等仍可能弱比较)。