CodeGym /课程 /JAVA 25 SELF /二进制序列化的问题:安全性与兼容性

二进制序列化的问题:安全性与兼容性

JAVA 25 SELF
第 45 级 , 课程 0
可用

1. 二进制序列化的安全性

在 Java 中,序列化并不只是保存对象的字段。这意味着只要实现了接口 Serializable,就可以“还原”任意对象及其任意内容。看起来很方便!但如果你的应用会反序列化来自不受信任来源的数据(例如来自网络,或被攻击者替换过的文件),就有可能成为攻击的受害者。

它是如何运作的?

在反序列化过程中,Java 会基于字节流创建对象,而不会调用类的构造函数。如果类实现了诸如 readObject 这样的特殊方法,它们会被自动调用。攻击者可以“构造”字节流,使其在反序列化时触发存在漏洞的代码。

示例:“gadget chain” 攻击

设想你有一个类,它在反序列化时会启动外部命令(读取文件或调用 shell)。如果攻击者知道应用会反序列化某些特定类型的对象,他就可以投递特制的字节流,从而导致恶意代码被执行。这类攻击通常被构造成“gadget 链”——由一系列调用组成,最终落到危险操作上。

为什么如此关键?

反序列化是一个将数据流还原为真实对象的过程,在这一刻可以执行任意代码。如果数据来自不受信任的来源,就为远程命令执行(RCE)打开了大门。大型公司曾发布过不安全反序列化的禁用建议;在现代企业项目中,二进制序列化常被安全策略禁止。

如何防护?

  • 切勿从不受信任来源反序列化对象。
  • 使用白名单——显式列出允许反序列化的类型。
  • 面向外部集成时优先采用文本格式:JSONXML
  • 如必须序列化,请使用带有安全反序列化设置的库(例如对类型进行限制的 Jackson)。
  • 限制使用非常规的序列化方法(readObjectreadResolve 等),除非你确信它们是安全的。

2. 类版本兼容性

Java 的二进制序列化与类结构紧密绑定。若在 1.0 版本中序列化了一个对象,随后又更新了类(添加/删除字段)——尝试将“旧”对象反序列化为新版本可能会导致错误或数据丢失。

Java 如何判定兼容性?

这依赖一个特殊字段——serialVersionUID。它是类的版本标识。如果序列化对象携带的 serialVersionUID 与当前类的不一致——会抛出 InvalidClassException,反序列化将不会进行。

import java.io.Serializable;

public class User implements Serializable {
    private static final long serialVersionUID = 1L; // 显式指定了版本

    private String name;
    private int age;
}

如果你更改了类的结构(例如新增字段 email)而未修改 serialVersionUID,Java 会认为该类是兼容的,并尝试反序列化旧对象。若未显式指定 serialVersionUID,JVM 会基于结构自动生成它,任何结构变动都会导致不兼容。

版本不匹配/匹配时会发生什么?

如果标识不匹配——反序列化不会进行:InvalidClassException。如果匹配——字段将按名称与类型进行映射:新增字段会获得默认值(null0),被删除的字段会被忽略。若更改了字段的类型或名称,可能会出现错误或数据解释不正确的情况。

实用建议。在可序列化的类中始终显式设置 serialVersionUID。仅在不兼容变更(删除/更改重要字段类型)时才修改它。新增字段时可以保留原有标识——JVM 会正确处理旧对象。

表:修改类时会发生什么

类中的变更 反序列化时的结果?
新增字段 将获得默认值(0null
删除字段 读取旧数据时被忽略
更改字段类型 抛出异常或得到不正确的数据
更改字段名称 旧字段被忽略,新字段为默认值
更改 serialVersionUID 抛出 InvalidClassException 异常

3. 标准序列化的限制

并非所有对象都能被序列化

带有 transientstatic 修饰符的字段不会被序列化。static 字段属于类而非对象;transient 表示你显式禁止该字段被序列化。

有些对象按定义就不可序列化:Thread、数据库连接、套接字、Scanner 等。如果你的类中包含此类类型的字段,且它不是 transient,将会得到 NotSerializableException

import java.io.Serializable;
import java.util.Scanner;

public class Session implements Serializable {
    private transient Scanner scanner; // 不会被序列化!
    private String login;
}

性能与可扩展性问题

序列化大型对象图可能很慢且占用大量内存。

二进制格式不适合与其他平台和语言进行集成——只有 Java 能“理解”它。

难以控制到底序列化了哪些内容,尤其是存在深层级的继承结构与循环引用时。

旧数据的维护问题

长期保存二进制快照存在风险。一两年后类结构发生变化,旧文件将无法加载。

真实案例:“我们三年前保存了用户缓存的序列化文件,升级了应用,而现在却无法加载它。和丢失的数据打个招呼吧!”

4. 最佳实践:如何避免陷阱

  • 仅将二进制序列化用于内部场景,并确保你控制流程的双方。
  • 不要将二进制序列化用于外部集成或长期存储重要数据。
  • 在可序列化的类中始终显式指定 serialVersionUID
  • 使用 transient 修饰不应被序列化的字段。
  • 与外部系统交互时——采用文本格式与现代库:JSONXMLJacksonGsonJAXB
  • 为兼容性采用版本化:在类中保存对象版本,并在反序列化时根据版本调整处理。
  • 若序列化仅用于缓存——不要不惜一切代价维持兼容性:缓存重建更容易。
  • 不要在可序列化对象中存放敏感数据(密码、密钥)——序列化不会加密数据。

5. 使用二进制序列化时的常见错误

错误 #1:从不受信任来源反序列化数据。 最危险的错误是接收并反序列化“来自外部”的对象(来自网络、用户或被替换的文件)。这会直接导致漏洞,甚至发展为 RCE。

错误 #2:在未更新 serialVersionUID 的情况下隐式更改类结构。 如果不显式指定标识,JVM 会自动基于结构生成它。任何结构变化(甚至字段顺序)都会导致不兼容,进而无法加载旧对象。

错误 #3:尝试序列化包含不可序列化字段的对象。 如果类中存在未实现 Serializable 的字段,且该字段不是 transient,序列化将以异常结束。

错误 #4:在可序列化对象中保存临时或敏感数据。 令牌、密码、临时资源描述符——这些都可能不慎落入文件中。

错误 #5:将二进制序列化用于长期存储以及跨版本交换。 类一旦升级,数据“损坏”和兼容性问题的风险就很高。

错误 #6:指望 statictransient 字段在反序列化后能被“恢复”。 这些字段不会被序列化;加载后它们将具有默认值。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION