Skip to content

JEP 415: Context-Specific Deserialization Filters | 上下文特定的反序列化过滤器

摘要

允许应用程序通过 JVM 范围内的过滤器工厂配置特定于上下文和动态选择的反序列化过滤器,该工厂被调用以为每个单独的反序列化操作选择过滤器。

非目标

  • 不定义反序列化过滤器选择策略。

  • 不定义过滤器配置或分发机制。

动机

反序列化不可信数据本质上是一项危险的活动,因为传入数据流的内容决定了要创建的对象、这些对象的字段值以及它们之间的引用。在许多典型用法中,流中的字节来自未知、不可信或未经身份验证的客户端。通过精心构造流,攻击者可以恶意执行任意类中的代码。如果对象构造具有改变状态或调用其他操作的副作用,则这些操作可能会破坏应用程序对象、库对象甚至 Java 运行时的完整性。禁用反序列化攻击的关键是阻止反序列化任意类的实例,从而防止直接或间接地执行它们的方法。

我们在 Java 9 中引入了 反序列化过滤器(JEP 290),使应用程序和库代码能够在反序列化之前 验证传入的数据流。此类代码在创建反序列化流(即 java.io.ObjectInputStream)时,会提供作为 java.io.ObjectInputFilter 的验证逻辑。

依赖流的创建者显式请求验证存在几个局限性。这种方法不具有可伸缩性,并且在代码发布后难以更新过滤器。此外,它也无法对应用程序中第三方库执行的反序列化操作施加过滤。

为了解决这些限制,JEP 290 还引入了一个可通过 API、系统属性或安全属性设置的 JVM 范围的反序列化过滤器。此过滤器是 静态的,因为它仅在启动时指定一次。使用静态 JVM 范围过滤器的经验表明,它也存在局限性,尤其是在具有多层库和多个执行上下文的复杂应用程序中。对每个 ObjectInputStream 使用 JVM 范围过滤器要求过滤器覆盖应用程序中的每个执行上下文,因此过滤器通常最终会过于宽泛或过于严格。

更好的方法是配置每个流的过滤器,而无需每个流的创建者参与。

为了保护 JVM 免受反序列化漏洞的影响,应用程序开发人员需要明确描述每个组件或库可以序列化或反序列化的对象。对于每个上下文和用例,开发人员应构建并应用适当的过滤器。例如,如果应用程序使用特定库来反序列化一组特定对象,那么在调用该库时,可以对该组相关类应用过滤器。创建一个类的允许列表,并拒绝其他所有内容,可以保护流中未知或意外的对象。可以使用封装或其他自然的应用程序或库分区边界来缩小允许或绝对不允许的对象集。如果不实用允许列表,则拒绝列表应包括已知不会出现在流中或已知是恶意的类、包和模块。

应用程序的开发人员最了解应用程序组件的结构和操作。此增强功能使应用程序开发人员能够构建并应用每个反序列化操作的过滤器。

描述

如上所述,JEP 290 引入了每流反序列化过滤器和静态 JVM 范围过滤器。每当创建 ObjectInputStream 时,其每流过滤器都会被初始化为静态 JVM 范围过滤器。如果需要,稍后可以更改该每流过滤器以使用不同的过滤器。

在这里,我们引入了可配置的 JVM 范围 过滤器工厂。每当创建 ObjectInputStream 时,其每流过滤器都会初始化为调用静态 JVM 范围过滤器工厂所返回的值。因此,这些过滤器是 动态的特定于上下文的,与单一的静态 JVM 范围反序列化过滤器不同。为了向后兼容,如果没有设置过滤器工厂,则内置的工厂将返回已配置的静态 JVM 范围过滤器(如果已配置)。

过滤器工厂用于 Java 运行时中的每个反序列化操作,无论是在应用程序代码、库代码还是 JDK 本身的代码中。该工厂特定于应用程序,并应考虑应用程序内的每个反序列化执行上下文。从 ObjectInputStream 构造函数和 ObjectInputStream.setObjectInputFilter 中调用过滤器工厂。参数是当前过滤器和新过滤器。从构造函数调用时,当前过滤器为 null,新过滤器为静态 JVM 范围过滤器。工厂确定并返回流的初始过滤器。工厂可以创建包含其他上下文特定控件的组合过滤器,或者仅返回静态 JVM 范围过滤器。如果调用了 ObjectInputStream.setObjectInputFilter,则会第二次调用工厂,参数是第一次调用返回的过滤器和请求的新过滤器。工厂确定如何组合这两个过滤器并返回过滤器,替换流上的过滤器。

对于简单情况,过滤器工厂可以为整个应用程序返回一个固定过滤器。例如,以下是一个允许示例类、允许 java.base 模块中的类,并拒绝所有其他类的过滤器:

java
var filter = ObjectInputFilter.Config.createFilter("example.*;java.base/*;!*")

在具有多个执行上下文的应用程序中,过滤器工厂可以通过为每个上下文提供自定义过滤器来更好地保护各个上下文。当构建流时,过滤器工厂可以根据当前线程局部状态、调用者层次结构、库、模块和类加载器来识别执行上下文。此时,用于创建或选择过滤器的策略可以根据上下文选择一个特定的过滤器或过滤器组合。

如果存在多个过滤器,则可以组合它们的结果。组合过滤器的一种有用方法是:如果任何过滤器拒绝反序列化,则拒绝;如果任何过滤器允许,则允许;否则保持未决定状态。

命令行使用

可以在命令行上设置属性 jdk.serialFilterjdk.serialFilterFactory 来设置过滤器和过滤器工厂。现有的 jdk.serialFilter 属性设置一个基于模式的过滤器。

jdk.serialFilterFactory 属性是在第一次反序列化之前要设置的过滤器工厂的类名。该类必须是公开的,并且可由应用程序类加载器访问。

为了与 JEP 290 兼容,如果未设置 jdk.serialFilterFactory 属性,则过滤器工厂将设置为内置工厂,以提供与早期版本的兼容性。

API

我们在 ObjectInputFilter.Config 类中定义了两个方法来设置和获取 JVM 范围内的过滤器工厂。过滤器工厂是一个带有两个参数的函数,这两个参数分别是当前过滤器和下一个过滤器,它返回一个过滤器。

java
/**
 * 返回 JVM 范围内的反序列化过滤器工厂。
 *
 * @return JVM 范围内的序列化过滤器工厂;非空
 */
public static BinaryOperator<ObjectInputFilter> getSerialFilterFactory();

/**
 * 设置 JVM 范围内的反序列化过滤器工厂。
 *
 * 过滤器工厂是一个双参数函数,这两个参数分别是当前过滤器和下一个过滤器,它返回用于流的过滤器。
 *
 * @param filterFactory 要设置为 JVM 范围内过滤器工厂的序列化过滤器工厂;不为空
 */
public static void setSerialFilterFactory(BinaryOperator<ObjectInputFilter> filterFactory);

示例

这个类展示了如何对当前线程中发生的每个反序列化操作进行过滤。它定义了一个线程局部变量来保存每个线程的过滤器,定义了一个过滤器工厂来返回该过滤器,将该工厂配置为 JVM 范围内的过滤器工厂,并提供了一个实用函数来在特定每个线程过滤器的上下文中运行 Runnable

java
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 类的简单示例:

java
// 创建一个 FilterInThread 过滤器工厂并设置
var filterInThread = new FilterInThread();
ObjectInputFilter.Config.setSerialFilterFactory(filterInThread);

// 创建一个过滤器以允许 example.*类并拒绝所有其他类
var filter = ObjectInputFilter.Config.createFilter("example.*;java.base/*;!*");
filterInThread.doWithSerialFilter(filter, () -> {
    byte[] bytes = ...;
    var o = deserializeObject(bytes);
});

备选方案

JEP 290 允许将过滤器实现为 Java 类,从而允许复杂的逻辑和上下文感知。可以通过在每个流上设置委托过滤器来实现依赖于上下文的特定流过滤器。为了确定特定流的过滤器,它需要检查调用者,并将调用者映射到特定过滤器,然后委托给该过滤器。但是,代码复杂性和确定调用者的开销都会影响每次调用的性能。