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_decode 和 unserialize 两个角度详细说明其与 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 == true → false,看似安全?
但更危险的是:如果后续逻辑未判断 $data === false,可能误将 false 当作有效数据处理。
更隐蔽的攻击(利用 false 参与数组访问):
// 假设代码:
$input = json_decode($_POST['data']);
if ($input && $input->is_admin == true) {
do_admin_stuff();
}
若攻击者提交非 JSON 数据(如空字符串、"false" 字符串等):
json_decode("")→NULLjson_decode("false")→false(布尔值)json_decode("0")→0json_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" == true → 1 == 1 → true!
✅ 结论:永远不要用弱比较判断布尔逻辑。
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 == 0 → true!
✅ 因为 "admin" 被转为整数 0,"0" 也是 0。
🔥 实际案例:很多 CTF 题和真实系统漏洞都源于此。
5.2 科学计数法绕过
if ($_GET['token'] == "999") {
unlock_feature();
}
攻击者提交:
?token=999e0
→ "999e0" 被解析为浮点数 999.0
→ 999.0 == "999" → 999 == 999 → true
更隐蔽的:
?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] == 1 → true!
因为在 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等仍可能弱比较)。