CodeGym /课程 /JAVA 25 SELF /序列化安全:最佳实践

序列化安全:最佳实践

JAVA 25 SELF
第 43 级 , 课程 4
可用

1. 安全序列化的核心最佳实践

序列化就像在机场打包行李:如果你不知道箱子里有什么、也不清楚把箱子交给了谁,安检时可能会遇到不愉快的惊喜。在 Java 中,序列化让我们能够轻松保存和恢复对象,但当数据来自不可靠来源时,也为各类攻击打开了大门。

典型威胁:

Java 中的序列化可能并不安全。如果攻击者注入恶意数据流,那么在反序列化时可能出现最糟糕的后果:从篡改字段到执行不期望的代码。这不是教材里的恐吓——Java 的历史上确实出现过基于该机制的攻击。

为什么会这样?

问题在于,反序列化不仅仅是恢复字段值。在此过程中会创建一个完整的对象:可能会调用特殊方法(例如 readObjectreadResolve),有时还会通过反射触达代码中的薄弱点。第三方库中的某些类尤其危险:它们可能在反序列化阶段就执行动作。因此,永远不要信任从外部获得的序列化数据。

使用 transient 处理敏感数据

如果你的类中包含存放密码、令牌、私钥或其他敏感信息的字段,请将其声明为 transient。这些数据不会进入序列化流。

import java.io.Serializable;

public class User implements Serializable {
    private String username;
    private transient String password; // 不会被序列化

    // ...构造器、getter、setter...
}

反序列化时会发生什么? 字段 password 将具有默认值(对于字符串是 null)。这很好:密码不会被存入文件或在网络上传输。

显式定义 serialVersionUID

务必显式声明 serialVersionUID。这可降低兼容性错误的概率,并将反序列化时的类替换风险降至最低。

private static final long serialVersionUID = 1L;

这对安全为何重要? 如果不指定 serialVersionUID,编译器会基于类结构自动生成。这可能导致意外的不匹配,理论上也会被用于通过同名但结构不同的类进行替换与滥用。

反序列化时校验对象类型

不要信任来自网络或文件的输入。在反序列化之后、开始使用对象之前,始终检查其是否为预期类型。

Object obj = objectInputStream.readObject();
if (obj instanceof User) {
    User user = (User) obj;
    // 安全地使用 user
} else {
    // 类型出乎意料,抛出异常或处理错误
}

为何需要这样做? 恶意数据流可能包含其他实现了 Serializable 的类对象,但并不符合你的业务逻辑。

限制可反序列化的类(ObjectInputFilter

从 Java 9 开始,使用过滤器 ObjectInputFilter 来限制允许反序列化的类集合。这就像入口处的门禁。

示例:设置过滤器

import java.io.*;

ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
        "com.example.User;com.example.Address;!*"
);

ObjectInputStream in = new ObjectInputStream(inputStream);
in.setObjectInputFilter(filter);

Object obj = in.readObject(); // 现在只有 User 和 Address 会被反序列化

该过滤器仅允许你应用中的 UserAddress 类。其他所有类都会被拦截——将抛出异常。这可显著降低恶意对象混入的风险。

不要反序列化来自不可信来源的数据

黄金法则:如果你无法确认数据的来源——就不要反序列化。优先选择在解析时不会执行代码的格式(例如 JSON、使用安全解析器的 XML)。

反例(不良实践):

// 千万不要对来自互联网的数据这样做!
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
Object obj = in.readObject(); // 危险!

更好的做法是什么?

  • 使用 JSON 解析器(例如 Gson/Jackson)或带验证的 XML 解析器。
  • 若必须使用二进制序列化,请通过 ObjectInputFilter 过滤类,并用 instanceof 校验类型。

与外部系统交互时使用替代格式

在系统集成时,优先使用解析时不执行代码的格式:JSON、XML、Protocol Buffers 等。这几乎可以杜绝通过反序列化进行的攻击。

// 使用 JSON 解析器替代 ObjectInputStream
User user = gson.fromJson(jsonString, User.class);

不要在公共位置存放序列化对象

包含序列化对象的文件可能含有敏感数据。不要将其存放在公共目录中,并在文件系统层面限制访问权限。

不要依赖序列化来保证完整性

序列化不保证数据的完整性或真实性。如果不允许被篡改,请使用数字签名、校验和或加密。

2. 实战:ObjectInputFilter 示例与漏洞演示

类过滤示例

假设我们有一个 User 类:

import java.io.Serializable;

public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private String username;
    private transient String password;

    // ...构造器、getter、setter...
}

过滤器只允许 User

ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
        "com.example.User;!*"
);
in.setObjectInputFilter(filter);

此时,一旦有人试图注入其他类的对象,反序列化将以错误告终。

潜在漏洞演示

恶意类:

// 假设有人塞入了这样一个类
public class Evil implements java.io.Serializable {
    static {
        System.out.println("恶意代码已执行!");
        // 这里可能是什么都有...
    }
}

如果不对类进行过滤,反序列化时可能会创建 Evil 对象,而静态初始化块会在类加载时执行——这就是一次真实的攻击。

4. 确保序列化安全时的常见错误

错误 1:未进行过滤和类型检查的反序列化。 开发者常常从流中读取对象后立即强制转换为需要的类型,这为攻击打开了大门。请使用 ObjectInputFilter,并通过 instanceof 进行类型检查。

错误 2:敏感数据未使用 transient 如果忘记将密码/密钥声明为 transient,它们就会进入数据流,并可能随着文件一同泄漏。

错误 3:缺少 serialVersionUID 没有显式的 serialVersionUID,容易出现意外的兼容性问题,也会带来类替换相关的风险。

错误 4:将序列化用于与外部系统的交换。 二进制序列化在应用内部(例如缓存)很方便,但对外部数据交换却很危险。优先选择 JSON/XML/Proto,并配合安全解析器。

错误 5:忽视数据完整性。 对序列化文件的字节做改动很可能不被发现。请使用数字签名、校验和或加密。

1
调查/小测验
序列化配置第 43 级,课程 4
不可用
序列化配置
序列化配置
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION