JEP 415: 上下文特定的反序列化过滤器
总结
允许应用程序通过 JVM 范围的过滤器工厂配置特定上下文和动态选择的反序列化过滤器,该工厂会在每次单独的反序列化操作时被调用以选择一个过滤器。
非目标
-
它的目标并非定义反序列化过滤器选择的策略。
-
它的目标并非定义过滤器配置或分发的机制。
动机
反序列化不受信任的数据是一种本质上危险的活动,因为传入数据流的内容决定了所创建的对象、其字段的值以及它们之间的引用。在许多典型用例中,流中的字节是从未知、不受信任或未经身份验证的客户端接收的。通过对流的精心构造,攻击者可以导致任意类中的代码以恶意意图被执行。如果对象构造具有改变状态或调用其他操作的副作用,这些操作可能会危及应用程序对象、库对象甚至 Java 运行时的完整性。禁用反序列化攻击的关键在于防止反序列化任意类的实例,从而防止直接或间接执行其方法。
我们在 Java 9 中引入了反序列化过滤器 (JEP 290),以使应用程序和库代码能够在反序列化之前验证传入的数据流。此类代码在创建反序列化流(即 java.io.ObjectInputStream
)时,会将验证逻辑作为 java.io.ObjectInputFilter
提供。
依赖流的创建者显式请求验证有几个限制。这种方法不具备扩展性,并且在代码发布后很难更新过滤器。它还无法在应用程序中由第三方库执行的反序列化操作上强制进行过滤。
为了解决这些限制,JEP 290 还引入了一个 JVM 范围的反序列化过滤器,可以通过 API、系统属性或安全属性进行设置。这个过滤器是静态的,因为它在启动时只被指定一次。通过使用静态的 JVM 范围过滤器发现,它同样存在限制,尤其是在具有多层库和多个执行上下文的复杂应用程序中。将 JVM 范围的过滤器用于每个 ObjectInputStream
,需要该过滤器覆盖应用程序中的每个执行上下文,因此过滤器通常要么过于宽松,要么过于严格。
一种更好的方法是配置每个流的过滤器,而不需要每个流创建者的参与。
为了保护 JVM 免受反序列化漏洞的影响,应用程序开发人员需要清楚地描述每个组件或库可以序列化或反序列化的对象。对于每个上下文和用例,开发人员应构建并应用适当的过滤器。例如,如果应用程序使用特定的库来反序列化特定的对象群组,则在调用库时可以应用相关类的过滤器。创建一个允许列表(allow-list)的类,并拒绝所有其他内容,这样可以防止流中出现未知或意外的对象。封装或其他自然的应用程序或库分区边界可用于缩小允许或明确不允许的对象集合。如果无法实际拥有一个允许列表,则拒绝列表(reject-list)应包括已知不会出现在流中的或者已知为恶意的类、包和模块。
应用程序的开发者最了解其组件的结构和操作。此增强功能使应用程序开发者能够构建过滤器并将其应用于每个反序列化操作。
描述
如上所述,JEP 290 引入了每流反序列化过滤器和静态 JVM 范围过滤器。每当创建 ObjectInputStream
时,其每流过滤器都会初始化为静态 JVM 范围过滤器。如果需要,可以随后将该每流过滤器更改为其他过滤器。
在此我们引入一个可配置的 JVM 范围的过滤器工厂。每当创建 ObjectInputStream
时,其每个流的过滤器都会初始化为通过调用静态 JVM 范围的过滤器工厂返回的值。因此,这些过滤器是动态的和上下文特定的,不同于单一的静态 JVM 范围的反序列化过滤器。为了向后兼容,如果没有设置过滤器工厂,则内置工厂会返回已配置的静态 JVM 范围的过滤器(如果有的话)。
过滤器工厂用于 Java 运行时中的每次反序列化操作,无论是在应用程序代码、库代码还是 JDK 本身中的代码。该工厂特定于应用程序,应考虑应用程序内的每个反序列化执行上下文。过滤器工厂从 ObjectInputStream
构造函数以及 ObjectInputStream.setObjectInputFilter
调用。参数为当前过滤器和新过滤器。当从构造函数调用时,当前过滤器为 null
,新过滤器为静态的整个 JVM 的过滤器。工厂确定并返回流的初始过滤器。工厂可以创建带有其他特定上下文控制的复合过滤器,或者仅返回静态的整个 JVM 的过滤器。如果调用了 ObjectInputStream.setObjectInputFilter
,则会使用第一次调用返回的过滤器和请求的新过滤器再次调用工厂。工厂决定如何组合这两个过滤器,并返回过滤器,替换流上的过滤器。
对于简单的情况,过滤器工厂可以为整个应用程序返回一个固定的过滤器。例如,以下是一个允许示例类、允许 java.base
模块中的类并拒绝所有其他类的过滤器:
var filter = ObjectInputFilter.Config.createFilter("example.*;java.base/*;!*")
在具有多个执行上下文的应用程序中,过滤器工厂可以通过为每个上下文提供自定义过滤器来更好地保护各个上下文。当流被构建时,过滤器工厂可以根据当前线程局部状态、调用者层次结构、库、模块和类加载器来识别执行上下文。此时,创建或选择过滤器的策略可以根据上下文选择特定的过滤器或过滤器组合。
如果存在多个过滤器,那么可以合并它们的结果。合并过滤器的一个有用方法是:如果任意一个过滤器拒绝反序列化,则拒绝;如果任意一个过滤器允许,则允许;否则保持未决状态。
命令行使用
属性 jdk.serialFilter
和 jdk.serialFilterFactory
可以在命令行中设置,以设置过滤器和过滤器工厂。现有的 jdk.serialFilter
属性设置基于模式的过滤器。
jdk.serialFilterFactory
属性是首次反序列化之前要设置的过滤器工厂的类名。该类必须是公共的,并且可被应用程序类加载器访问。
为了与 JEP 290 兼容,如果未设置 jdk.serialFilterFactory
属性,则过滤器工厂将设置为一个提供与早期版本兼容性的内置工厂。
API
我们在 ObjectInputFilter.Config
类中定义了两个方法,用于设置和获取整个 JVM 的过滤器工厂。过滤器工厂是一个带有两个参数的函数,这两个参数分别是当前过滤器和下一个过滤器,并返回一个过滤器。
/**
* Return the JVM-wide deserialization filter factory.
*
* @return the JVM-wide serialization filter factory; non-null
*/
public static BinaryOperator<ObjectInputFilter> getSerialFilterFactory();
/**
* Set the JVM-wide deserialization filter factory.
*
* The filter factory is a function of two parameters, the current filter
* and the next filter, that returns the filter to be used for the stream.
*
* @param filterFactory the serialization filter factory to set as the
* JVM-wide filter factory; not null
*/
public static void setSerialFilterFactory(BinaryOperator<ObjectInputFilter> filterFactory);
示例
这个类展示了如何过滤当前线程中发生的每个反序列化操作。它定义了一个线程局部变量来保存每个线程的过滤器,定义了一个过滤器工厂来返回该过滤器,将该工厂配置为整个 JVM 的过滤器工厂,并提供了一个实用函数来在特定每线程过滤器的上下文中运行 Runnable
。
public class FilterInThread implements BinaryOperator<ObjectInputFilter> {
// ThreadLocal to hold the serial filter to be applied
private final ThreadLocal<ObjectInputFilter> filterThreadLocal = new ThreadLocal<>();
// Construct a FilterInThread deserialization filter factory.
public FilterInThread() {}
/**
* The filter factory, which is invoked every time a new ObjectInputStream
* is created. If a per-stream filter is already set then it returns a
* filter that combines the results of invoking each filter.
*
* @param curr the current filter on the stream
* @param next a per stream filter
* @return the selected filter
*/
public ObjectInputFilter apply(ObjectInputFilter curr, ObjectInputFilter next) {
if (curr == null) {
// Called from the OIS constructor or perhaps OIS.setObjectInputFilter with no current filter
var filter = filterThreadLocal.get();
if (filter != null) {
// Prepend a filter to assert that all classes have been Allowed or Rejected
filter = ObjectInputFilter.rejectUndecidedClass(filter);
}
if (next != null) {
// Prepend the next filter to the thread filter, if any
// Initially this is the static JVM-wide filter passed from the OIS constructor
// Append the filter to reject all UNDECIDED results
filter = ObjectInputFilter.merge(next, filter);
filter = ObjectInputFilter.rejectUndecidedClass(filter);
}
return filter;
} else {
// Called from OIS.setObjectInputFilter with a current filter and a stream-specific filter.
// The curr filter already incorporates the thread filter and static JVM-wide filter
// and rejection of undecided classes
// If there is a stream-specific filter prepend it and a filter to recheck for undecided
if (next != null) {
next = ObjectInputFilter.merge(next, curr);
next = ObjectInputFilter.rejectUndecidedClass(next);
return next;
}
return curr;
}
}
/**
* Apply the filter and invoke the runnable.
*
* @param filter the serial filter to apply to every deserialization in the thread
* @param runnable a Runnable to invoke
*/
public void doWithSerialFilter(ObjectInputFilter filter, Runnable runnable) {
var prevFilter = filterThreadLocal.get();
try {
filterThreadLocal.set(filter);
runnable.run();
} finally {
filterThreadLocal.set(prevFilter);
}
}
}
如果已经通过 ObjectInputStream::setObjectFilter
设置了特定于流的过滤器,那么过滤器工厂会将该过滤器与下一个过滤器结合。如果任一过滤器拒绝某个类,则该类会被拒绝。如果任一过滤器允许该类,则该类会被允许。否则,结果未定。
下面是一个使用 FilterInThread
类的简单示例:
// Create a FilterInThread filter factory and set
var filterInThread = new FilterInThread();
ObjectInputFilter.Config.setSerialFilterFactory(filterInThread);
// Create a filter to allow example.* classes and reject all others
var filter = ObjectInputFilter.Config.createFilter("example.*;java.base/*;!*");
filterInThread.doWithSerialFilter(filter, () -> {
byte[] bytes = ...;
var o = deserializeObject(bytes);
});
替代方案
JEP 290 允许将过滤器实现为 Java 类,从而允许复杂的逻辑和上下文感知。通过使用委托过滤器(该过滤器设置在每个流上),可以实现依赖于上下文的特定流的过滤器。为了确定特定流的过滤器,需要检查其调用者,将调用者映射到特定的过滤器,然后委托给该过滤器。然而,代码复杂性和确定调用者的开销都会影响每次调用的性能。