JEP 415:上下文特定的反序列化过滤器
概括
允许应用程序通过 JVM 范围的过滤器工厂配置特定于上下文和动态选择的反序列化过滤器,调用该工厂为每个单独的反序列化操作选择过滤器。
非目标
-
定义反序列化过滤器选择策略不是目标。
-
定义过滤器的配置或分发机制不是目标。
动机
反序列化不受信任的数据本质上是一项危险的活动,因为传入数据流的内容决定了创建的对象、其字段的值以及它们之间的引用。在许多典型用途中,流中的字节是从未知、不受信任或未经身份验证的客户端接收的。通过仔细构建流,攻击者可以恶意地导致任意类中的代码被执行。如果对象构造具有更改状态或调用其他操作的副作用,那么这些操作可能会损害应用程序对象、库对象甚至 Java 运行时的完整性。禁用反序列化攻击的关键是防止任意类的实例被反序列化,从而防止直接或间接执行其方法。
我们在 Java 9 中引入了反序列化过滤器 (JEP 290),以使应用程序和库代码能够在反序列化之前验证传入的数据流。此类代码在java.io.ObjectInputFilter
创建反序列化流(即 a java.io.ObjectInputStream
)时提供验证逻辑。
依赖流的创建者显式请求验证有几个限制。这种方法无法扩展,并且在代码发布后很难更新过滤器。它也无法对应用程序中第三方库执行的反序列化操作进行过滤。
为了解决这些限制,JEP 290 还引入了 JVM 范围的反序列化过滤器,可以通过 API、系统属性或安全属性进行设置。该过滤器是_静态的_,因为它在启动 时只指定一次。静态 JVM 范围过滤器的经验表明,它也有局限性,特别是在具有库层和多个执行上下文的复杂应用程序中。对 every 使用 JVM 范围的过滤器ObjectInputStream
需要过滤器覆盖应用程序中的每个执行上下文,因此过滤器通常会变得过于包容或过于严格。
更好的方法是以不需要每个流创建者参与的方式配置每个流过滤器。
为了保护 JVM 免受反序列化漏洞的影响,应用程序开发人员需要对每个组件或库可以序列化或反序列化的对象进行清晰的描述。对于每个上下文和用例,开发人员应该构建并应用适当的过滤器。例如,如果应用程序使用特定的库来反序列化特定的一组对象,则在调用该库时可以应用相关类的过滤器。创建类的允许列表并拒绝其他所有内容,可以防止流中未知或意外的对象。封装或其他自然应用程序或库分区边界可用于缩小允许或绝对不允许的对象集。如果允许列表不切实际,则拒绝列表应包括已知不会出现在流中或已知是恶意的类、包和模块。
应用程序的开发人员最有能力了解应用程序组件的结构和操作。此增强功能使应用程序开发人员能够构建过滤器并将其应用于每个反序列化操作。
描述
如上所述,JEP 290 引入了每流反序列化过滤器和静态 JVM 范围的过滤器。每当创建 an 时ObjectInputStream
,其每流过滤器都会被初始化为静态 JVM 范围的过滤器。如果需要,以后可以将该每个流的过滤器更改为不同的过滤器。
这里我们介绍一个可配置的 JVM 范围的_过滤器工厂_。每当创建 an 时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
未设置属性,过滤器工厂将设置为提供与早期版本兼容性的内置函数。
应用程序编程接口
我们在类中定义了两个方法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 类,从而允许复杂的逻辑和上下文感知。上下文相关的特定于流的过滤器可以通过使用在每个流上设置的委托过滤器来实现。要确定特定流的过滤器,需要检查其调用者并将调用者映射到特定过滤器,然后委托给该过滤器。然而,代码复杂性和确定调用者的开销都会影响每次调用的性能。