
PHP序列化格式详解:从基础语法到安全防护的完整教程
一、什么是序列化?为什么要用它?
在PHP开发中,我们经常需要把内存中的数据保存到文件、数据库或者通过网络传给另一个程序。然而,数组、对象这些复杂结构无法直接被存储或传输,必须转换成一种线性的、可还原的字符串形式。这个过程就叫序列化,反向还原的过程叫反序列化。
举个生活中的例子:你想把一套乐高积木寄给朋友,不能直接把拼好的模型塞进信封,得先拆散成零件,附上一张图纸,朋友收到后再按图纸重新组装。序列化就是那个“拆散+画图纸”的动作,反序列化就是“按图纸重新拼装”。
PHP内置了两个核心函数:serialize()负责编码,unserialize()负责解码。它们生成的字符串遵循一套固定的语法规则,任何PHP环境都能正确解析。这套规则正是我们今天要深入学习的重点。
二、序列化字符串的总体结构
所有PHP序列化后的数据都以一个类型标识符开头,后面跟着具体的数值或内容。常见的类型标识符如下:
类型 | 标识符 | 示例 |
|---|---|---|
字符串 |
|
|
整数 |
|
|
布尔值 |
|
|
浮点数 |
|
|
NULL |
|
|
数组 |
|
|
对象 |
|
|
每个序列化片段都以分号结尾(数组和对象内部也有自己的闭合结构)。掌握了这个总纲,你就能读懂任何序列化字符串的含义。
三、基本类型的序列化详解
1. 字符串
字符串的格式为:s:长度:"内容";
$str = "PHP序列化";
echo serialize($str); // 输出:s:10:"PHP序列化";注意:长度是按字节计算的。在UTF-8编码下,“PHP序列化”共10个字节(P、H、P各1字节,三个汉字各3字节,合计10)。如果字符串中包含双引号或反斜杠,序列化时会自动转义,但长度仍按转义前的字符计算。
2. 整数
格式:i:数值;
$int = 42;
echo serialize($int); // i:42;整数范围取决于PHP的整数位数(32位或64位),超出范围会变成浮点数序列化。
3. 布尔值
格式:b:1;表示true,b:0;表示false。
$flag = true;
echo serialize($flag); // b:1;注意:布尔值序列化后只有0和1两种可能,不会出现b:true;这种写法。
4. 浮点数
格式:d:数值;
$pi = 3.14159;
echo serialize($pi); // d:3.14159;浮点数在序列化时保持原样,但要注意精度问题。特别大的浮点数可能会用科学计数法表示,比如d:1.23456789E+15;。
5. NULL
格式:N;非常简单,只有一个字母加一个分号。
$nothing = null;
echo serialize($nothing); // N;四、数组的序列化
数组是最常用的复合类型之一。它的序列化格式为:
a:元素个数:{键序列化值序列化键序列化值序列化...}注意:键和值是交替出现的,而且键也会被序列化成对应的类型(通常是整数或字符串)。
$arr = [
"name" => "张三",
"age" => 28,
"skills" => ["PHP", "MySQL"]
];
echo serialize($arr);
// 输出:
// a:3:{s:4:"name";s:6:"张三";s:3:"age";i:28;s:6:"skills";a:2:{i:0;s:3:"PHP";i:1;s:5:"MySQL";}}我们来逐层解析:
a:3:表示这是一个有3个元素的数组。- 第一个键值对:键
s:4:"name"(字符串长度4),值s:6:"张三"。 - 第二个键值对:键
s:3:"age",值i:28。 - 第三个键值对:键
s:6:"skills",值又是一个数组a:2:{...}。 - 内层数组有两个元素,索引0对应字符串"PHP",索引1对应字符串"MySQL"。
反序列化时,unserialize()会原封不动地恢复这个嵌套结构。
五、对象的序列化
对象比数组复杂一些,因为它还包含了类的信息。格式如下:
O:类名长度:"类名":属性数量:{属性序列化...}其中属性的序列化方式取决于访问修饰符(public、protected、private)。不同修饰符在序列化时的键名有特殊约定。
1. 公有属性
class User {
public $id = 1001;
public $role = "admin";
}
$user = new User();
echo serialize($user);
// 输出:O:4:"User":2:{s:2:"id";i:1001;s:4:"role";s:5:"admin";}公有属性的键名就是属性名本身,前面加上s:长度:。
2. 受保护属性
受保护属性在序列化时,键名会被修改为:\0*\0属性名(注意:\0是空字节)。例如:
class Admin {
protected $level = 3;
}
$obj = new Admin();
echo serialize($obj);
// 输出:O:5:"Admin":1:{s:9:"\0*\0level";i:3;}这里s:9:表示空字节+星号+空字节+level共9个字节。空字节在显示时通常看不到,但在字符串中是真实存在的。
3. 私有属性
私有属性更特殊,键名为:\0类名\0属性名。例如:
class Secret {
private $token = "abc123";
}
$obj = new Secret();
echo serialize($obj);
// 输出:O:6:"Secret":1:{s:15:"\0Secret\0token";s:6:"abc123";}注意:类名前后各有一个空字节,总长度为类名字节数+2(两个空字节)+属性名字节数。
了解这些细节非常重要,因为很多反序列化漏洞正是利用了这些特殊字符来伪造属性。
4. 反序列化时的类加载
当你调用unserialize()时,PHP会尝试创建对应类的实例。如果该类尚未被定义,PHP会创建一个__PHP_Incomplete_Class对象,它虽然保留了数据,但无法正常调用方法。因此,反序列化之前一定要确保所需的类已经被加载(可以通过自动加载机制实现)。
六、反序列化安全:你必须知道的陷阱
序列化本身只是一个数据转换工具,但反序列化却可能成为攻击者的突破口。核心原因在于:反序列化过程中,PHP会自动触发某些魔术方法,例如__wakeup()、__destruct()、__toString()等。如果攻击者精心构造一个序列化字符串,使得这些方法中执行了危险操作(如执行系统命令、写入文件、SQL查询),就可能造成远程代码执行(RCE)。
常见攻击手法
假设有一个类:
class Evil {
public $cmd = "";
function __destruct() {
system($this->cmd);
}
}攻击者可以构造这样的序列化字符串:
$payload = 'O:4:"Evil":1:{s:3:"cmd";s:6:"whoami";}';
unserialize($payload);当脚本结束或对象被销毁时,__destruct()就会被调用,执行system("whoami")。这就是最简单的反序列化RCE。
更隐蔽的利用链
真实的漏洞往往不止一个类,而是通过多个类的魔术方法串联成一条“gadget chain”。比如某个框架的类A的__wakeup()会调用类B的方法,类B的方法又会调用类C的__call(),最终执行任意代码。攻击者需要深入研究目标程序的类结构,找到可利用的链条。
如何防御?
- 永远不要反序列化不可信的数据。来自用户输入、网络请求、外部文件的序列化字符串都应视为有毒。
- 使用白名单类限制。PHP 7.0+ 的
unserialize()允许传入第二个参数allowed_classes,指定只允许反序列化哪些类。这样即使攻击者传入了Evil类,也会被忽略。 - 改用JSON代替序列化。如果只是简单数据交换,
json_encode/json_decode更安全,且跨语言兼容。 - 对序列化字符串进行签名。使用HMAC算法对序列化内容生成摘要,反序列化前验证签名是否匹配,防止篡改。
- 定期更新PHP版本。旧版本中存在许多已知的反序列化漏洞修复。
七、实战场景与最佳实践
场景1:Session存储
默认情况下,PHP Session使用序列化格式保存到文件中。你可以通过session.serialize_handler配置修改处理器。但要注意,如果Session数据被篡改,可能导致权限提升。建议开启Session加密或使用Redis等安全存储。
场景2:缓存复杂数据
Memcached、Redis等缓存系统常配合序列化存储数组或对象。此时应确保缓存键名不可预测,防止用户伪造key读取敏感数据。
场景3:API接口数据传输
如果要在不同系统间传递结构化数据,优先考虑JSON而不是序列化。JSON更轻量、跨语言、无代码执行风险。只有在需要保留对象类型(如DateTime对象)时才使用序列化,并且要对端点做严格的认证授权。
编码建议
- 自定义序列化方法:实现
Serializable接口或定义__sleep()和__wakeup()方法,可以精确控制哪些属性被序列化,以及在反序列化后如何初始化。 - 避免在
__wakeup()中执行危险操作,如数据库查询、文件写入。 - 使用
is_a()或instanceof检查反序列化后的对象类型。
八、总结
PHP序列化是一把双刃剑:它让数据持久化和跨进程传输变得极其方便,但同时也打开了安全潘多拉魔盒。掌握序列化语法能帮助你调试代码、理解框架底层原理;而重视反序列化安全则能避免服务器沦陷。记住三条黄金法则:
- 能用JSON就别用序列化。
- 必须用时,严格限定可反序列化的类。
- 永远别信用户提交的序列化字符串。
希望这篇教程能让你对PHP序列化有一个全面而深刻的认识。动手写几个例子试试吧,实践是最好的老师。
serializeunserialize反序列化安全数据持久化魔术方法修改时间:2026-08-02 05:18:03