把收到的字节还原成对象之所以危险,不是因为内容格式不对。在某些格式中,外部数据可以决定构造哪种类型的对象。
为什么输入校验来得太晚
纯数据格式(JSON / XML)
外部数据只决定值 → 你接收后,再映射到自己的类型 → 校验有效
还原对象的格式
外部数据决定值和类型 → 还原过程中运行所选类型的代码 → 执行根本到不了你的检查
大多数输入校验问的是值能不能接受。但在还原对象的格式中,还原本身——实例化哪个类、过程中运行什么代码——是由外部数据操纵的。在你的校验被调用之前,某些东西可能就已经执行完了。
它和原型污染是同一种形态:外部数据决定的是结构而不是值。名称不同、语言不同,防御的思路相同。
会出什么问题(OWASP 列出的三项)
- 拒绝服务
- 让还原本身变得开销很大,或使其出错,从而让应用停止
- 绕过访问控制
- 表示权限或角色的对象被构造成对攻击者有利的样子
- 远程代码执行
- 把还原过程中调用的例程串起来,达到任意代码执行——最严重的结果
「我们的框架不会这样」不是一个安全的假设
这不只是旧语言的问题。我们统计 NVD 数据中的 Next.js 漏洞时,反序列化(CWE-502)在 CWE 分布中排名靠前——因为现代框架自带反序列化请求体的路径,Server Actions 就是其中之一(Next.js 和 React 的漏洞多吗?)。「不是我写的」并不意味着「它不存在」。
两种防御
1. 不让任何东西选择类型(首选)
OWASP 的原话:「通过改用 JSON 或 XML 这样的纯数据格式,可以降低自定义反序列化逻辑被挪作恶意用途的可能性。」
用只携带值的格式接收外部输入,然后在自己的代码中映射到自己的类型。仅此一项,就能阻止攻击者选择要构造什么。在大多数代码库中,这就能解决问题。
2. 签名,并拒绝未签名的
在不得不使用自定义格式的地方,指南指出可以「在序列化过程中对其签名」,然后「选择不反序列化任何没有经过认证的签名的消息」。
关键是先验证再还原。由于上面说的原因,先还原再检查可能已经太晚了。顺序就是全部的控制。
有些机制无法通过配置来保护
对于 .NET 的 BinaryFormatter,OWASP 直截了当地说:「BinaryFormatter 类型是危险的,无法被保护」。
这句话在实际运维中很重要:确实存在「只要正确使用就没问题」根本不成立的工具。加固选项、增加校验、编写允许列表——都不能让它变得安全。唯一的控制就是不用它。
所以在审查时,首先要把配置能保护的和不能保护的分开。在后一类上花力气不是缓解,而是拖延。
OWASP 按语言点名的内容
- PHP
- 避免使用
unserialize();优先使用 JSON - Python
pickle/c_pickle、PyYAML 的load和jsonpickle被列为危险- Java
- 重写
ObjectInputStream#resolveClass(),限制可以还原的类 - .NET
BinaryFormatter是危险的,无法被保护——不要使用
共同点是:语言自带的方便的持久化机制,恰恰是绝不能面对外部数据的东西。它们是为了在相互信任的双方之间传递完整对象而设计的,并不假定发送方怀有恶意。
各语言中安全与不安全的 API
下表列出了各语言官方文档警告不要用于外部数据的东西,以及推荐的替代方案。如果左列中的东西出现在你的代码里,请查清它的数据来自哪里。
| 语言 | 不要用于外部数据 | 改用 | 官方文档的说法 |
|---|---|---|---|
| Python | pickle.load / pickle.loads、marshal、yaml.load(配合 Loader=yaml.Loader)和 yaml.unsafe_load、jsonpickle.decode | json.loads、yaml.safe_load | pickle「不安全」;如果需要防篡改,用 hmac 签名 |
| PHP | unserialize()(即使设置了 allowed_classes) | json_decode() / json_encode() | 无论 allowed_classes 如何,都不要传入不可信的输入 |
| Ruby | Marshal.load | JSON,或其他只加载基本类型的格式 | 从不可信的来源加载可能导致远程代码执行 |
| Java | 对外部数据使用 ObjectInputStream#readObject、XMLDecoder、XStream#fromXML | 把 JSON 映射到自己的类(DTO) | 如果无法避免,用 ObjectInputFilter 限制允许的类 |
| .NET | BinaryFormatter、SoapFormatter、NetDataContractSerializer、LosFormatter、ObjectStateFormatter、Json.NET 中 None 以外的 TypeNameHandling | System.Text.Json、XmlSerializer、DataContractSerializer | 从 .NET 9 起,使用 BinaryFormatter 会抛出异常 |
放在一起对比,区别是这样的。不安全的写法把收到的东西直接还原成对象;安全的写法把它作为值读取,只把需要的字段移到自己的类型中。
# 避免:把收到的字节直接还原成对象
order = pickle.loads(request_body)
# 使用:读取值,然后只把需要的字段移到自己的类型中
data = json.loads(request_body)
order = Order(id=int(data["id"]), quantity=int(data["quantity"]))在安全的写法中,由你的代码(Order)决定构造哪个类。传入的数据只决定 id 和 quantity 的值,普通的输入校验对它们有效。
在自己的代码中要检查什么
列出外部数据最初到达的每一个地方
请求体、Cookie、会话、队列、上传、Webhook、缓存——所有先保存、之后再还原的地方。会话和缓存最容易被遗漏:你以为是自己的数据,取决于路径,也可能从外部到达。
找出其中哪些使用了还原类型的格式
搜索上面列表中点名的函数和类。它们是最先要去掉的东西。如果正在使用无法被保护的机制(BinaryFormatter 等),那就是最高优先级——只有迁移才能解决。
迁移到纯数据格式
接收 JSON 或 XML,在自己的代码中映射到自己的类型。在映射过程中进行模式(schema)校验,可以同时堵住意外的键(与原型污染的对策相同)。
无法迁移的地方,用签名保护
如果格式无法改变,就在序列化时签名,在还原前验证。把**「没有签名,就不还原」**作为默认规则。密钥的管理方式见.env 和 API 密钥的危险在哪里——绝不要和代码放在同一个地方。
统计依赖包所做的还原
即使你自己一处都没写,依赖包也可能在还原对象。被归类为 CWE-502 的 CVE 不断出现,所以要交给机器监控(osv-scanner 入门/实用的 CVE 修复手册)。
如何检查自己的环境
这一节把上面步骤中「找出来」的部分变成具体的命令和设置。所有检查都只读取你自己的仓库和你自己的服务器。
对源代码做文本搜索
先把所有候选列出来。用 ripgrep(rg)搜索的写法如下(grep -rnE 也可以使用相同的模式)。
# Python
rg -n "pickle\.loads?\(|marshal\.loads?\(|yaml\.load\(|yaml\.unsafe_load|jsonpickle\.decode"
# PHP
rg -n "unserialize\("
# Ruby
rg -n "Marshal\.load"
# Java
rg -n "ObjectInputStream|readObject\(|XMLDecoder|fromXML\("
# .NET
rg -n "BinaryFormatter|SoapFormatter|NetDataContractSerializer|LosFormatter|ObjectStateFormatter|TypeNameHandling"并不是每一处命中都危险。对每一处,写下它接收的数据来自哪里,优先处理连接到外部可到达路径的那些:请求、Cookie、上传、共享存储。
查找被压制的警告
在 .NET 中,使用 BinaryFormatter 会产生 SYSLIB0011 警告或错误。压制它的配置,标示出代码中仍然留有危险调用的位置。
rg -n "SYSLIB0011|CA23[0-3][0-9]" --glob "*.cs" --glob "*.csproj" --glob ".editorconfig" --glob "*.props"如果发现了 #pragma warning disable SYSLIB0011,或者 <NoWarn> 中有这个 ID,请确认它存在的原因以及何时迁移。
开启静态分析规则
对于 Python,相关的 Bandit 检查是 B301(pickle 等)、B302(marshal)和 B506(yaml.load);bandit -r . -t B301,B302,B506 只运行这三项。对于 .NET,相关的代码分析规则是 CA2300 系列(例如标记 Json.NET 的 TypeNameHandling 的 CA2326)。即使在 .NET 10 中,CA2326 默认也是不启用的,所以要在 .editorconfig 中用 dotnet_diagnostic.CA2326.severity = warning 这样的一行来开启。在 CI 中运行这些检查,新的调用也会被拦下。
对于 Java,检查运行时过滤器的设置
Java 可以在运行时限制反序列化可以创建哪些类(序列化过滤器)。该机制在 JDK 9 中引入(JEP 290),并在 Java 17 中可以按上下文切换(JEP 415)。对于正在运行的进程,在 jcmd <PID> VM.system_properties 的输出中查找 jdk.serialFilter。它也可以作为安全属性写在 java.security 文件中,所以两处都要检查。如果两处都没有,而且你的代码也没有调用 ObjectInputFilter.Config.setSerialFilter,就说明没有进程级的过滤器。
检查依赖包的已知漏洞
即使你自己的代码里没有这样的调用,依赖包也可能在还原对象。用 osv-scanner 或类似工具列出依赖包中的已知漏洞,优先更新被归类为 CWE-502(不可信数据的反序列化)的那些。
常见错误及修正方法
| 错误 | 为什么有问题 | 修正 |
|---|---|---|
| 因为「是内部的」或「这个文件是我自己保存的」而信任数据 | Microsoft 举出了数据在开发者没注意到的情况下越过信任边界的例子:共享的存档文件、云同步、被入侵的员工电脑 | 只要有任何从外部到达的路径,就当作外部数据处理。不还原任何未签名的东西 |
| 把危险的类一个个加进黑名单 | 对不在名单上或尚未知晓的组合毫无作用。PHP 手册说即使设置了 allowed_classes 也不要传入不可信的输入 | 把格式改为 JSON 等。只在无法避免时,列出允许的类 |
| 先还原,再校验内容 | 还原过程中就会运行代码,所以校验来得太晚 | 把签名验证放在还原之前 |
| 压制警告就当作修好了 | 让 SYSLIB0011 或 CA2326 不再报警,危险的调用依然留在原处 | 搜索被压制的地方,并设定迁移期限 |
| 以为「是 JSON 所以安全」 | 让 JSON 自己指定类型的设置,例如 Json.NET 的 TypeNameHandling,会把它变成会选择类型的格式 | 让 TypeNameHandling 保持为 None(默认值) |
在 PyYAML 中使用 yaml.load | PyYAML 的文档说 yaml.load 和 pickle 一样强大,可能调用任何 Python 函数 | 换成 yaml.safe_load |
真实案例:扩展内部的还原
本站关于 CVE-2026-45247 的快讯,介绍了 Magento 2 扩展「Mirasvit Full Page Cache Warmer」1.11.12 之前版本中的 PHP 对象注入(CWE-502)。它无需认证即可导致远程代码执行,CVSS 评分 9.3,并已被列入 CISA 的已知被利用漏洞目录(KEV)。
教训在于,还原的代码不是网站运营者写的,而是在他们安装的扩展里。这就是上面「检查依赖包」那一步重要的原因。修复方法是更新扩展,搜索自己的代码是找不到它的。
出处(一手资料)
- OWASP Cheat Sheet Series, "Deserialization Cheat Sheet" — cheatsheetseries.owasp.org(三项影响、改用纯数据格式、为完整性签名、各语言的注意事项以及关于
BinaryFormatter的说明,均出自该文档;2026 年 9 月 5 日确认) - Python 文档,"pickle" — docs.python.org(「不安全」的警告;用
hmac签名、优先使用 JSON) - PyYAML Documentation — pyyaml.org(
yaml.load与yaml.safe_load的区别) - PHP 手册,"unserialize" — php.net(无论
allowed_classes如何都不要传入不可信的输入;使用 JSON) - Ruby 文档,"Marshal" — docs.ruby-lang.org
- Microsoft Learn, "Deserialization risks in use of BinaryFormatter and related types" — learn.microsoft.com(同样危险的类型、推荐的替代方案、.NET 9 起的行为、越过信任边界的例子)
- Microsoft Learn, "SYSLIB0011" 和 "CA2326" — SYSLIB0011 / CA2326
- OpenJDK, "JEP 290: Filter Incoming Serialization Data" 和 "JEP 415: Context-Specific Deserialization Filters" — JEP 290 / JEP 415
- Bandit 文档(B301、B302、B506)— bandit.readthedocs.io
- 各语言对照表、搜索步骤和常见错误,已于 2026 年 10 月 6 日对照上述文档确认
接下来阅读
- 同样的形态:Node.js 中的原型污染(外部数据决定结构)/文件上传漏洞(外部数据决定东西落在哪里)
- 术语:什么是远程代码执行(RCE)
- 真实案例:CVE-2026-45247(Magento 扩展中的 PHP 对象注入)
- 按框架:ASP.NET Core/Spring Boot/Django 的安全
- 依赖包:osv-scanner 入门/实用的 CVE 修复手册
FAQ
Q反序列化为什么这么危险?不就是数据转换吗?
因为在某些格式中,它不是数据转换,而是对象重建。当「要实例化哪个类」的信息随数据一起传过来时,攻击者就能选择你要构造什么。OWASP 指出,针对反序列化器的攻击曾被发现可以导致拒绝服务、访问控制绕过或远程代码执行。难以防御之处在于,攻击在你的值检查运行之前就已经成功了。
Q最可靠的防御是什么?
把来自外部的数据限制为无法选择类型的格式。OWASP 是这样说的:通过改用 JSON 或 XML 这样的纯数据格式,可以降低自定义反序列化逻辑被挪作恶意用途的可能性。如果不得不使用自定义格式,同一份指南指出,可以在序列化过程中对消息签名,然后选择不反序列化任何没有经过认证的签名的消息。先改格式;改不了就用签名约束。
Q能通过加固配置让它变得安全吗?
取决于机制。OWASP 明确指出,.NET 的 BinaryFormatter 类型是危险的,无法被保护。有些机制不会因为设置或额外的校验而变得安全——唯一的解决办法是停止使用。要把配置能保护的和不能保护的分开。「只要正确使用就没问题」并不成立的工具,确实存在。
Q在我使用的语言里要注意什么?
OWASP 直接点名的有:PHP 中避免使用 unserialize(),优先使用 JSON;Python 中 pickle / c_pickle、PyYAML 的 load 和 jsonpickle 是危险的;Java 中重写 ObjectInputStream#resolveClass() 来限制可以还原的类;.NET 中不要使用 BinaryFormatter。共同点是:语言自带的方便的持久化机制,恰恰是绝不能用于外部数据的东西。
Q如何在自己的代码中找出不安全的反序列化?
分三步。第一,用 ripgrep 或 grep 对 pickle.loads、unserialize、Marshal.load、ObjectInputStream、BinaryFormatter 等调用做文本搜索。第二,开启静态分析规则(Python 用 Bandit 的 B301、B302 和 B506;.NET 用 CA2300 系列的代码分析规则)。第三,用 osv-scanner 等工具扫描依赖包的已知漏洞,优先更新 CWE-502 类的漏洞。对找到的每一处调用,写下它接收的数据来自哪里,先修复从外部可以到达的那些。
Q只要用 JSON 就一定安全吗?
如果你开启了让数据自己指定类型的设置,就不安全。常见的例子是 .NET 的 Json.NET 中的 TypeNameHandling;Microsoft 的代码分析规则 CA2326 要求不要使用 None 以外的任何值。JSON 的好处在于它只携带值,所以由接收方的代码决定构造哪个类。请确认你没有开启会抵消这个好处的设置。