1. 安全序列化的核心最佳实践
序列化就像在机场打包行李:如果你不知道箱子里有什么、也不清楚把箱子交给了谁,安检时可能会遇到不愉快的惊喜。在 Java 中,序列化让我们能够轻松保存和恢复对象,但当数据来自不可靠来源时,也为各类攻击打开了大门。
典型威胁:
Java 中的序列化可能并不安全。如果攻击者注入恶意数据流,那么在反序列化时可能出现最糟糕的后果:从篡改字段到执行不期望的代码。这不是教材里的恐吓——Java 的历史上确实出现过基于该机制的攻击。
为什么会这样?
问题在于,反序列化不仅仅是恢复字段值。在此过程中会创建一个完整的对象:可能会调用特殊方法(例如 readObject、readResolve),有时还会通过反射触达代码中的薄弱点。第三方库中的某些类尤其危险:它们可能在反序列化阶段就执行动作。因此,永远不要信任从外部获得的序列化数据。
使用 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 会被反序列化
该过滤器仅允许你应用中的 User 和 Address 类。其他所有类都会被拦截——将抛出异常。这可显著降低恶意对象混入的风险。
不要反序列化来自不可信来源的数据
黄金法则:如果你无法确认数据的来源——就不要反序列化。优先选择在解析时不会执行代码的格式(例如 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:忽视数据完整性。 对序列化文件的字节做改动很可能不被发现。请使用数字签名、校验和或加密。
GO TO FULL VERSION