1. PHP类型系统基础
1.1 动态类型与弱类型
定义: PHP是一种动态类型 且弱类型的语言。动态类型意味着变量类型在运行时确定;弱类型意味着不同类型之间的操作会触发自动类型转换。
通俗类比: 把PHP变量想象成一个可以变形的容器:
- 你可以往里面放字符串,它会变成字符串容器
- 你把它和数字相加,它会变成数字容器
- 这种变形是自动发生的,不需要你显式声明
PHP的8种数据类型:
| 类型 | 示例 | 说明 |
|---|---|---|
| boolean | true, false |
布尔值 |
| integer | 42, -10 |
整数 |
| float | 3.14, 1.0e10 |
浮点数 |
| string | "hello", 'world' |
字符串 |
| array | [1, 2, 3] |
数组/哈希表 |
| object | new Class() |
对象 |
| resource | fopen(...) |
资源句柄 |
| null | null |
空值 |
1.2 类型转换的触发场景
在PHP中,类型转换可能在以下场景自动发生:
php
<?php
// 场景1: 算术运算
$result = "10" + 5; // int(15) - 字符串转为整数
$result = "10.5" * 2; // float(21) - 字符串转为浮点数
// 场景2: 比较运算
$equal = "10" == 10; // bool(true) - 字符串转为整数比较
$equal = "0" == false; // bool(true) - 都转为布尔值
// 场景3: 字符串连接
$text = "Number: " . 42; // string("Number: 42") - 数字转为字符串
// 场景4: 条件判断
if ("non-empty") { // 字符串转为布尔值true
// 执行
}
// 场景5: 函数参数
strlen(123); // int(3) - 数字转为"123"
// 场景6: 数组键
$arr["10"] = "value"; // 键被转为整数10
$arr[10] = "other"; // 覆盖上一个值!
?>
1.3 类型转换的优先级
当不同类型进行运算时,PHP会按照以下规则进行转换:
布尔值 < 整数 < 浮点数 < 字符串
转换规则表:
| 操作数1 | 操作数2 | 转换结果 |
|---|---|---|
| 整数 | 浮点数 | 整数 → 浮点数 |
| 字符串 | 整数 | 字符串 → 整数 |
| 布尔值 | 任何类型 | 布尔值 → 对应类型 |
| null | 字符串 | null → 空字符串 "" |
| 数组 | 任何类型 | 数组保持不变(特殊情况) |
2. 类型转换机制详解
2.1 字符串到数字的转换
这是最常见的类型转换,也是安全问题的重灾区。
php
<?php
// 转换规则:从字符串开头提取数字直到遇到非数字字符
var_dump("123abc" + 0); // int(123)
var_dump("123.45xyz" + 0); // float(123.45)
var_dump("abc123" + 0); // int(0) - 开头不是数字,结果为0
var_dump("" + 0); // int(0) - 空字符串转为0
var_dump(" 42 " + 0); // int(42) - 忽略前后空格
// 十六进制和八进制(PHP 5.x行为,7.0+已变更)
var_dump("0x1A" + 0); // PHP 5.x: int(26), PHP 7+: int(0)
var_dump("0777" + 0); // int(777) - 不会解释为八进制
// 科学计数法
var_dump("1e3" + 0); // float(1000)
var_dump("1.5e-2" + 0); // float(0.015)
?>
安全启示:
- 🔴
0和"0"在比较时相等,但0和"abc"也相等(因为后者转为0) - 🔴 用户输入的
"0"可能被误认为是false - 🟢 始终使用严格比较
===来避免意外转换
2.2 布尔值转换规则
php
<?php
// 以下值会被转换为 false:
$falseValues = [
false, // 布尔值false
0, // 整数0
0.0, // 浮点数0.0
"", // 空字符串
"0", // 字符串"0"
[], // 空数组
null, // null
// 未定义变量(警告)
];
// 以下值会被转换为 true:
$trueValues = [
true, // 布尔值true
1, // 非零整数
-1, // 负数
0.1, // 非零浮点数
" ", // 含空格的字符串(不是空字符串!)
"0.0", // 字符串"0.0"(不是"0"!)
"false", // 字符串"false"
[0], // 非空数组
new stdClass(), // 对象
];
// 危险示例
$userInput = "0"; // 用户输入了字符串"0"
if ($userInput) { // 结果为false!
// 这行不会执行
}
?>
2.3 数组类型转换
php
<?php
// 数组在字符串上下文中的行为
$arr = [1, 2, 3];
echo "Array: " . $arr; // 输出 "Array: Array"(转换警告)
// 数组在布尔上下文中的行为
if ([1, 2, 3]) { // true - 非空数组为真
echo "Truthy";
}
if ([]) { // false - 空数组为假
echo "Never reached";
}
// 数组键的类型转换
$arr = [];
$arr["1"] = "string key"; // 键被转为整数1
$arr[1] = "integer key"; // 覆盖!
var_dump($arr); // array(1) { [1]=> string(11) "integer key" }
$arr["01"] = "not one"; // 键保持字符串"01"(前导零)
var_dump($arr); // 现在有2个元素
?>
2.4 对象类型转换
php
<?php
class Stringable {
public function __toString(): string {
return "I am a string";
}
}
$obj = new Stringable();
echo $obj; // 输出 "I am a string"
// 没有__toString方法的对象
general = new stdClass();
echo $general; // 错误:无法转换为字符串
// 对象到数组的转换
$arr = (array)$obj; // 转换为包含对象属性的数组
?>
3. 弱类型比较的安全陷阱
3.1 松散比较 () vs 严格比较 (=)
这是PHP安全编程中最关键的区分点。
php
<?php
// 松散比较 (==): 类型转换后比较值
var_dump("10" == 10); // bool(true) - 字符串转为数字
var_dump("0e123" == "0"); // bool(true) - 科学计数法!
var_dump("0x0" == 0); // PHP 5.x: true, PHP 7+: false
var_dump(true == "hello"); // bool(true) - 字符串转为true
var_dump(false == ""); // bool(true) - 都转为false
// 严格比较 (===): 值和类型都相同
var_dump("10" === 10); // bool(false) - 类型不同
var_dump(10 === 10); // bool(true)
?>
安全检查清单:
- ✅ 永远优先使用
===和!== - ✅ 比较用户输入前,先进行显式类型转换
- ✅ 验证返回值时,注意
0、"0"、false的区别
3.2 科学计数法陷阱
这是PHP中最微妙的类型安全问题之一。
php
<?php
// MD5哈希碰撞示例
$hash1 = md5("QNKCDZO"); // 0e830400451993494058024219903391
$hash2 = md5("240610708"); // 0e462097431906509019562988736854
// 危险!两个不同的字符串,但松散相等
var_dump($hash1 == $hash2); // bool(true)!
// 原因:"0e..." 被解释为 0 * 10^... = 0
// 同样的攻击适用于任何哈希算法
$sha1_1 = sha1("aaroZmOk"); // 0e6650701996942713489456749430518516575
$sha1_2 = sha1("aaK1STfY"); // 0e7665852665575620768827115962402601134
var_dump($sha1_1 == $sha1_2); // bool(true)!
// 正确的验证方式
var_dump($hash1 === $hash2); // bool(false) - 严格比较
?>
防御方案:
php
<?php
// ✅ 安全的哈希比较
function secureHashCompare(string $hash1, string $hash2): bool {
// 方法1: 严格比较
return $hash1 === $hash2;
// 方法2: 使用hash_equals(抗时序攻击)
return hash_equals($hash1, $hash2);
}
// ✅ 验证哈希格式
function isValidHash(string $hash, int $length = 32): bool {
// 检查是否为指定长度的十六进制字符串
return strlen($hash) === $length && ctype_xdigit($hash);
}
?>
3.3 JSON类型陷阱
php
<?php
// JSON解码默认返回stdClass对象或数组
$json = '{"id": 123, "is_admin": false}';
$data = json_decode($json);
// 危险:访问不存在的属性返回null
if ($data->is_admin) { // false,不会执行
// ...
}
if ($data->is_super_admin) { // null,转为false,不会执行(但有警告)
// ...
}
// 更好的做法
if (isset($data->is_admin) && $data->is_admin === true) {
// 明确检查
}
// JSON中的数字精度问题
$json = '{"big_number": 9007199254740993}';
$data = json_decode($json);
var_dump($data->big_number); // 可能是 9007199254740992(精度丢失)
?>
3.4 字符串与布尔值的混淆
php
<?php
// 表单验证中的常见陷阱
$input = $_POST['accept_terms']; // 用户未勾选,没有发送该字段
if ($input == false) {
// 这里不会执行!因为$input是null,不是false
// 实际应该提示用户必须接受条款
}
// 正确的检查
if (!isset($input) || $input !== 'yes') {
echo "You must accept the terms";
}
// 另一个常见陷阱
$search = $_GET['q']; // 用户搜索"0"
if ($search) { // "0"转为false!
// 搜索逻辑不会执行
performSearch($search);
}
// 正确做法
if (isset($_GET['q']) && strlen($_GET['q']) > 0) {
performSearch($_GET['q']);
}
?>
4. 类型混淆攻击面
4.1 身份验证绕过
php
<?php
// 危险代码示例
function isValidUser($userId) {
// 期望 $userId 是整数
$user = getUserFromDatabase($userId);
return $user !== null;
}
// 攻击
$userId = "0"; // 字符串"0"
if (isValidUser($userId)) {
// 可能绕过检查,取决于数据库查询实现
}
// 更危险的例子
function checkAdmin($userId) {
$admins = [1, 5, 10]; // 管理员ID列表
return in_array($userId, $admins); // 松散比较!
}
// 攻击
$userId = "1 something"; // 字符串
var_dump(checkAdmin($userId)); // bool(true)!
// 原因:"1 something" 转为整数1,在数组中找到
// 修复
function checkAdminSafe($userId) {
$admins = [1, 5, 10];
return in_array($userId, $admins, true); // 严格比较
}
?>
4.2 权限控制绕过
php
<?php
// 基于类型的权限检查绕过
class User {
public $role = 'user';
}
class Admin {
public $role = 'admin';
}
// 不安全的权限检查
function hasPermission($user, $permission) {
if ($user->role == 'admin') { // 松散比较
return true;
}
// 检查具体权限...
}
// 攻击
$fake = new stdClass();
$fake->role = true; // true == 'admin' 吗?
var_dump(hasPermission($fake, 'delete'));
// 结果取决于具体PHP版本和上下文
// 安全的权限检查
function hasPermissionSafe($user, $permission) {
if (!isset($user->role) || !is_string($user->role)) {
return false;
}
if ($user->role === 'admin') {
return true;
}
// ...
}
?>
4.3 SQL注入与类型
php
<?php
// 依赖类型转换的SQL注入
$userId = $_GET['id']; // 攻击者发送 "1 OR 1=1"
// 危险的假设:intval可以安全地处理
$query = "SELECT * FROM users WHERE id = " . intval($userId);
// 实际上intval("1 OR 1=1") = 1,这里安全
// 但如果忘记使用intval:
$query = "SELECT * FROM users WHERE id = $userId";
// 直接注入:SELECT * FROM users WHERE id = 1 OR 1=1
// 更隐蔽的情况
$order = $_GET['order']; // 期望 "asc" 或 "desc"
$query = "SELECT * FROM products ORDER BY price $order";
// 攻击:?order=;DROP TABLE products;--
// 安全的做法
$allowedOrders = ['asc' => 'ASC', 'desc' => 'DESC'];
if (!isset($allowedOrders[$order])) {
$order = 'asc';
}
$query = "SELECT * FROM products ORDER BY price " . $allowedOrders[$order];
?>
4.4 反序列化类型混淆
php
<?php
// 对象属性类型混淆
class User {
public $isAdmin = false; // 期望布尔值
}
// 恶意序列化数据
$payload = 'O:4:"User":1:{s:8:"isAdmin";s:4:"true";}';
$user = unserialize($payload);
var_dump($user->isAdmin); // string("true"),不是布尔值true!
// 但在松散比较中
if ($user->isAdmin == true) { // string("true") == true → true
// 权限提升!
}
// 安全的反序列化
class SecureUser {
private $isAdmin = false;
public function __wakeup() {
// 强制类型
$this->isAdmin = (bool)$this->isAdmin;
}
public function isAdmin(): bool {
return $this->isAdmin === true;
}
}
?>
5. 严格类型模式
5.1 declare(strict_types=1)
PHP 7.0引入的严格类型模式是防御类型相关漏洞的重要工具。
php
<?php
declare(strict_types=1);
// 在严格模式下,类型必须完全匹配
function add(int $a, int $b): int {
return $a + $b;
}
echo add(1, 2); // 正常: 3
echo add("1", "2"); // 错误: 必须传递整数
echo add(1.5, 2.5); // 错误: 浮点数不能自动转为整数
// 严格模式只影响调用时的类型检查,不影响函数内部
?>
重要说明:
strict_types指令作用于调用方所在的文件- 必须在文件最开头声明,在任何代码之前
- 只影响该文件中的函数调用,不影响被调用的函数
php
<?php
// file1.php - 非严格模式
function greet(string $name) {
echo "Hello, $name";
}
greet(123); // 正常,自动转为"123"
?>
php
<?php
declare(strict_types=1);
// file2.php - 严格模式
require 'file1.php';
greet(123); // 错误!严格模式要求传递字符串
greet("John"); // 正常
?>
5.2 标量类型声明
php
<?php
declare(strict_types=1);
// 支持的标量类型
function process(
bool $flag, // 布尔值
int $count, // 整数
float $price, // 浮点数
string $name // 字符串
): array { // 返回类型
return [
'flag' => $flag,
'count' => $count,
'price' => $price,
'name' => $name
];
}
// 联合类型 (PHP 8.0+)
// function format(string|int $input): string { ... }
// 可为空类型
function findUser(?int $id): ?array {
if ($id === null) return null;
// ...
}
?>
5.3 严格类型的安全价值
php
<?php
declare(strict_types=1);
// 场景1: 防止科学计数法攻击
function verifyHash(string $expected, string $actual): bool {
return hash_equals($expected, $actual);
}
// 现在必须传递字符串,不能传递可能被误解释的数值
// 场景2: 防止类型混淆
function setUserId(int $id): void {
$this->userId = $id;
}
// 场景3: 明确的数据库交互
function getUserById(int $userId): ?array {
$stmt = $this->db->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$userId]); // 保证是整数
return $stmt->fetch() ?: null;
}
// 场景4: API输入验证
class UserController {
public function update(int $id, string $name, ?string $email): array {
// 参数类型已经确保了基本的数据类型安全
// 只需要进行业务逻辑验证
if (strlen($name) < 2) {
throw new ValidationException("Name too short");
}
if ($email !== null && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new ValidationException("Invalid email");
}
// ...
}
}
?>
6. 防御性类型检查
6.1 输入验证模式
php
<?php
/**
* 类型安全的输入验证类
*/
class TypedInput {
/**
* 获取整数,失败返回null
*/
public static function int(array $source, string $key): ?int {
if (!isset($source[$key])) return null;
if (!is_numeric($source[$key])) return null;
$int = (int)$source[$key];
return (string)$int === (string)$source[$key] ? $int : null;
}
/**
* 获取字符串
*/
public static function string(array $source, string $key, int $maxLen = 255): ?string {
if (!isset($source[$key]) || !is_string($source[$key])) {
return null;
}
$str = trim($source[$key]);
return strlen($str) <= $maxLen ? $str : null;
}
/**
* 获取布尔值(支持多种表示)
*/
public static function bool(array $source, string $key): bool {
if (!isset($source[$key])) return false;
$val = $source[$key];
return $val === true || $val === '1' || $val === 'true' || $val === 'on';
}
/**
* 从枚举中选择
*/
public static function enum(array $source, string $key, array $allowed): ?string {
$val = self::string($source, $key);
return $val !== null && in_array($val, $allowed, true) ? $val : null;
}
}
// 使用示例
$userId = TypedInput::int($_GET, 'id') ?? 0;
$search = TypedInput::string($_GET, 'q', 100);
$order = TypedInput::enum($_GET, 'order', ['asc', 'desc']) ?? 'asc';
?>
6.2 过滤器函数
php
<?php
// filter_var 是PHP内置的类型安全验证工具
// 验证整数
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if ($id === false || $id === null) {
throw new InvalidArgumentException("Invalid ID");
}
// 验证范围
$age = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT);
if ($age === false || $age < 0 || $age > 150) {
throw new InvalidArgumentException("Invalid age");
}
// 验证布尔值
$active = filter_input(INPUT_POST, 'active', FILTER_VALIDATE_BOOLEAN);
// 注意:FILTER_VALIDATE_BOOLEAN 对"false"字符串也返回true
// 验证邮箱
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
if ($email === false) {
throw new InvalidArgumentException("Invalid email");
}
// 验证URL
$url = filter_input(INPUT_POST, 'url', FILTER_VALIDATE_URL);
// 自定义过滤器(正则)
$username = filter_input(INPUT_POST, 'username', FILTER_VALIDATE_REGEXP, [
'options' => ['regexp' => '/^[a-zA-Z0-9_]{3,20}$/']
]);
?>
6.3 安全的比较模式
php
<?php
/**
* 安全比较函数集合
*/
class SafeCompare {
/**
* 安全字符串比较(抗时序攻击)
*/
public static function strings(string $a, string $b): bool {
return hash_equals($a, $b);
}
/**
* 安全整数比较
*/
public static function ints($a, $b): bool {
if (!is_int($a) || !is_int($b)) {
return false;
}
return $a === $b;
}
/**
* 检查值是否在允许列表中
*/
public static function inAllowed($value, array $allowed): bool {
return in_array($value, $allowed, true);
}
/**
* 验证数组键是否存在且值匹配预期类型
*/
public static function hasKey(array $arr, string $key, string $type): bool {
if (!array_key_exists($key, $arr)) {
return false;
}
$value = $arr[$key];
return match($type) {
'int' => is_int($value),
'string' => is_string($value),
'bool' => is_bool($value),
'array' => is_array($value),
'null' => $value === null,
default => false
};
}
}
?>
6.4 防御编程检查清单
php
<?php
/**
* 输入处理安全检查清单的实现
*/
class SecurityChecklist {
/**
* 验证用户ID
*/
public static function validateUserId($id): int {
// 检查是否为null
if ($id === null) {
throw new SecurityException("User ID cannot be null");
}
// 检查是否为整数或数字字符串
if (!is_numeric($id)) {
throw new SecurityException("User ID must be numeric");
}
// 转换为整数
$intId = (int)$id;
// 验证转换后的一致性
if ((string)$intId !== (string)$id) {
throw new SecurityException("User ID contains invalid characters");
}
// 验证范围
if ($intId <= 0) {
throw new SecurityException("User ID must be positive");
}
return $intId;
}
/**
* 验证布尔标志
*/
public static function validateBool($value): bool {
// 明确只接受特定的布尔表示
if (is_bool($value)) {
return $value;
}
if ($value === 1 || $value === '1' || $value === 'true') {
return true;
}
if ($value === 0 || $value === '0' || $value === 'false' || $value === '') {
return false;
}
throw new SecurityException("Invalid boolean value");
}
/**
* 验证枚举值
*/
public static function validateEnum($value, array $allowed) {
if (!in_array($value, $allowed, true)) {
throw new SecurityException("Value not in allowed set");
}
return $value;
}
}
?>