1. Introduction
1.导言
In this tutorial, we’ll learn about Java’s new Context-Specific Deserialization Filter functionality. We’ll establish a scenario and then use it in practice to determine what deserialization filters should be used for each situation in our application.
在本教程中,我们将学习 Java 新的特定上下文反序列化过滤器功能。我们将建立一个场景,然后在实践中使用它来确定在应用程序中的每种情况下应使用什么样的反序列化过滤器。
2. How It Relates to JEP 290
2.与 JEP 290 的关系
JEP 290 was introduced in Java 9 to filter deserialization from external sources through a JVM-wide filter and the possibility to define a filter for each ObjectInputStream instance. These filters rejected or allowed an object to be deserialized based on runtime parameters.
JEP 290 在 Java 9 中引入,通过 JVM 范围的过滤器和为每个 ObjectInputStream 实例定义过滤器的可能性来过滤来自外部源的反序列化。这些过滤器根据运行时参数拒绝或允许对象被反序列化。
The dangers of deserializing untrusted data have long been debated, and mechanisms to help with that have also been improving. So, we now have more options for dynamically choosing deserialization filters, and it’s easier to create them.
反序列化不受信任的数据的危险性长期以来一直备受争议,帮助解决这一问题的机制也在不断改进。因此,我们现在有了更多动态选择反序列化筛选器的选项,而且创建筛选器也更加容易。
3. New Methods in ObjectInputFilter With JEP 415
3.使用 JEP 的 ObjectInputFilter 中的新方法 415
To give more options on how and when to define deserialization filters, JEP 415 introduced in Java 17 the ability to specify a JVM-wide filter factory invoked every time a deserialization occurs. This way, our solutions for filtering don’t become too restrictive or too broad anymore.
为了在定义反序列化筛选器的方式和时间上提供更多选择,JEP 415 在 Java 17 中引入了指定 JVM 范围筛选器工厂的功能,每次反序列化发生时都会调用该工厂。
Also, to give more context control, there are new methods that ease the creation and combination of filters:
此外,为了提供更多的上下文控制,还提供了新的方法来简化过滤器的创建和组合:
- rejectFilter(Predicate<Class<?>> predicate, Status otherStatus): rejects the deserialization if the predicate returns true, otherStatus otherwise
- allowFilter(Predicate<Class<?>> predicate, Status otherStatus): allows deserialization if the predicate returns true, otherStatus otherwise
- rejectUndecidedClass(ObjectInputFilter filter): maps every UNDECIDED return from the filter passed to REJECTED, with a few exceptional cases
- merge(ObjectInputFilter filter, ObjectInputFilter anotherFilter): tries to test both filters but returns REJECTED on the first REJECTED status it gets. It’s also null-safe for anotherFilter, returning the filter itself instead of a new, combined filter
Note: rejectFilter() and allowFilter() return UNDECIDED if information about the class being deserialized is null.
注意:如果被反序列化类的信息为null,rejectFilter()和allowFilter()将返回UNDECIDED。
4. Building Our Scenario and Setup
4.构建我们的场景和设置
To illustrate our deserialization filter factory’s job, our scenario will involve a few POJOs serialized somewhere else and deserialized by our application via a few different service classes. We’ll use these to simulate situations where we can block potentially unsafe deserialization of external sources. Ultimately, we’ll learn how to define parameters to detect unexpected properties in serialized content.
为了说明反序列化过滤器工厂的工作,我们的场景将涉及一些 POJOs 序列化到其他地方,并由我们的应用程序通过一些不同的服务类进行反序列化。我们将使用这些类来模拟可以阻止外部源的潜在不安全反序列化的情况。最后,我们将学习如何定义参数以检测序列化内容中的意外属性。
Let’s start with our POJOs’ marker interface:
让我们从 POJO 的 marker 接口开始:
public interface ContextSpecific extends Serializable {}
Firstly, our Sample class will contain basic properties that are checkable during deserialization through ObjectInputFilter, like arrays and nested objects:
首先,我们的 Sample 类将包含可在反序列化过程中通过 ObjectInputFilter 检查的基本属性,如数组和嵌套对象:
public class Sample implements ContextSpecific, Comparable<Sample> {
private static final long serialVersionUID = 1L;
private int[] array;
private String name;
private NestedSample nested;
public Sample(String name) {
this.name = name;
}
public Sample(int[] array) {
this.array = array;
}
public Sample(NestedSample nested) {
this.nested = nested;
}
// standard getters and setters
@Override
public int compareTo(Sample o) {
if (name == null)
return -1;
if (o == null || o.getName() == null)
return 1;
return getName().compareTo(o.getName());
}
}
We’re only implementing Comparable to add our instances to a TreeSet later. It’ll help in showing how code can be executed indirectly. Secondly, we’ll use our NestedSample class to change the depth of our deserialized objects, which we’ll use to set a limit on how deep an object graph can be before deserialization:
我们实现 Comparable 只是为了稍后将我们的实例添加到 TreeSet 中。这将有助于展示如何间接执行代码。其次,我们将使用 NestedSample 类来更改反序列化对象的深度,我们将使用它来设置反序列化前对象图的深度限制:
public class NestedSample implements ContextSpecific {
private Sample optional;
public NestedSample(Sample optional) {
this.optional = optional;
}
// standard getters and setters
}
Finally, let’s create a simple exploit example to filter out later. It contains side effects in its toString() and compareTo() methods, which, for example, can be indirectly called by TreeSet every time we add items to it:
最后,让我们创建一个简单的漏洞利用示例,以便稍后进行过滤。它的 toString() 和 compareTo() 方法中包含副作用,例如,每次我们向 TreeSet 中添加项目时,这些方法都会被 TreeSet 间接调用: TreeSet 的 toString() 和 compareTo() 方法中包含副作用。
public class SampleExploit extends Sample {
public SampleExploit() {
super("exploit");
}
public static void maliciousCode() {
System.out.println("exploit executed");
}
@Override
public String toString() {
maliciousCode();
return "exploit";
}
@Override
public int compareTo(Sample o) {
maliciousCode();
return super.compareTo(o);
}
}
Note that this simple example is for illustration purposes only and doesn’t aim to emulate a real-world exploit.
请注意,这个简单的示例仅供参考,并不旨在模拟真实世界中的漏洞。
4.1. Serialization and Deserialization Utilities
4.1.序列化和反序列化实用程序
To facilitate our test cases later, let’s create a few utilities to serialize and deserialize our objects. We’ll start with simple serialization:
为了方便以后的测试用例,让我们创建几个实用程序来序列化和反序列化我们的对象。我们将从简单的 序列化开始:
public class SerializationUtils {
public static void serialize(Object object, OutputStream outStream) throws IOException {
try (ObjectOutputStream objStream = new ObjectOutputStream(outStream)) {
objStream.writeObject(object);
}
}
}
Again, to help with our tests, we’ll create a method that deserializes all non-rejected objects into a set, along with a deserialize() method that optionally receives another filter:
同样,为了帮助我们进行测试,我们将创建一个方法,将所有未被拒绝的对象反序列化到一个集合中,同时创建一个 deserialize() 方法,该方法可选择接收另一个过滤器:
public class DeserializationUtils {
public static Object deserialize(InputStream inStream) {
return deserialize(inStream, null);
}
public static Object deserialize(InputStream inStream, ObjectInputFilter filter) {
try (ObjectInputStream in = new ObjectInputStream(inStream)) {
if (filter != null) {
in.setObjectInputFilter(filter);
}
return in.readObject();
} catch (InvalidClassException e) {
return null;
}
}
public static Set<ContextSpecific> deserializeIntoSet(InputStream... inputStreams) {
return deserializeIntoSet(null, inputStreams);
}
public static Set<ContextSpecific> deserializeIntoSet(
ObjectInputFilter filter, InputStream... inputStreams) {
Set<ContextSpecific> set = new TreeSet<>();
for (InputStream inputStream : inputStreams) {
Object object = deserialize(inputStream, filter);
if (object != null) {
set.add((ContextSpecific) object);
}
}
return set;
}
}
Note that, for our scenario, we’re returning null when an InvalidClassException happens. This exception is thrown every time any filter rejects a deserialization. That way, we don’t break deserializeIntoSet() since we’re only interested in collecting successful deserializations and discarding the others.
请注意,在我们的应用场景中,当发生 InvalidClassException 时,我们将返回 null 。 这样,我们就不会破坏 deserializeIntoSet() ,因为我们只对收集成功的反序列化感兴趣,而对其他反序列化则不感兴趣。
4.2. Creating Filters
4.2.创建过滤器
Before building a filter factory, we need some filters to choose from. We’ll create a few simple filters using ObjectInputFilter.Config.createFilter(). It receives a pattern of accepted or rejected packages, along with a few parameters to check before an object is deserialized:
在创建过滤器工厂之前,我们需要一些过滤器供选择。我们将使用 ObjectInputFilter.Config.createFilter() 创建几个简单的过滤器。它接收接受或拒绝包的模式,以及在对象反序列化之前要检查的几个参数: ObjectInputFilter.Config.createFilter()
public class FilterUtils {
private static final String DEFAULT_PACKAGE_PATTERN = "java.base/*;!*";
private static final String POJO_PACKAGE = "com.baeldung.deserializationfilters.pojo";
// ...
}
We start by setting DEFAULT_PACKAGE_PATTERN with a pattern to accept any classes from the “java.base” module and reject anything else. Then, we set POJO_PACKAGE with the package that contains the classes in our application that need deserialization.
首先,我们将 DEFAULT_PACKAGE_PATTERN 设置为接受来自 “java.base “模块的任何类并拒绝其他任何类的模式。然后,我们将 POJO_PACKAGE 设置为包含应用程序中需要反序列化的类的包。
With that information, let’s create methods to serve as a basis for our filters. With baseFilter(), we’ll receive the name of the parameter we want to check, along with a maximum value:
有了这些信息,让我们创建方法来作为我们过滤器的基础。通过 baseFilter() 方法,我们将收到要检查的参数名称以及最大值: baseFilter() 方法将收到要检查的参数名称以及最大值。
private static ObjectInputFilter baseFilter(String parameter, int max) {
return ObjectInputFilter.Config.createFilter(String.format(
"%s=%d;%s.**;%s", parameter, max, POJO_PACKAGE, DEFAULT_PACKAGE_PATTERN));
}
// ...
And, with fallbackFilter(), we’ll create a more restrictive filter that only accepts classes from DEFAULT_PACKAGE_PATTERN. It’ll be used for deserializations outside of our service classes:
我们将使用 fallbackFilter() 创建一个限制性更强的过滤器,它只接受来自 DEFAULT_PACKAGE_PATTERN 的类。它将用于服务类之外的反序列化:
public static ObjectInputFilter fallbackFilter() {
return ObjectInputFilter.Config.createFilter(String.format("%s", DEFAULT_PACKAGE_PATTERN));
}
Finally, let’s write the filters that we’ll use to restrict the number of bytes read, the array sizes in our objects, and the maximum depth of the object graph for deserialization:
最后,让我们来编写过滤器,用于限制读取的字节数、对象中的数组大小以及对象图的最大深度,以便进行反序列化: <br
public static ObjectInputFilter safeSizeFilter(int max) {
return baseFilter("maxbytes", max);
}
public static ObjectInputFilter safeArrayFilter(int max) {
return baseFilter("maxarray", max);
}
public static ObjectInputFilter safeDepthFilter(int max) {
return baseFilter("maxdepth", max);
}
And with all that setup, we’re ready to start writing our filter factory.
完成所有设置后,我们就可以开始编写我们的过滤器工厂了。
5. Creating a Deserialization Filter Factory
5.创建反序列化过滤器工厂
A deserialization filter factory allows us to dynamically select a specific filter depending on what’s being deserialized instead of relying on a single filter for the entire application. Or, setting a different one every time we create an ObjectInputStream instance. We can now have many context-specific filters and choose or combine them during runtime.
反序列化过滤器工厂允许我们根据反序列化的内容动态选择特定的过滤器,而不是依赖整个应用程序的单一过滤器。或者在每次创建 ObjectInputStream 实例时设置不同的过滤器。我们现在可以拥有许多特定于上下文的过滤器,并在运行时选择或组合它们。
The mechanism for this involves implementing a BinaryOperator<ObjectInputFilter> and then setting its class name via the jdk.serialFilterFactory JVM property, or by calling ObjectInputFilter.Config.setSerialFilterFactory(). The factory is JVM-wide and can only be set once. So, if it’s set via the JVM property, it cannot be replaced programmatically. Also, for security reasons, it cannot be set to null.
其机制包括实现 二进制操作符<对象输入过滤器>,然后通过 jdk.serialFilterFactory JVM 属性或调用 ObjectInputFilter.Config.setSerialFilterFactory() 设置其类名。该工厂是 JVM 范围内的,只能设置一次。因此,如果它是通过 JVM 属性设置的,就不能通过编程进行替换。此外,出于安全原因,它不能被设置为空。
5.1. Strategy for Choosing Filters
5.1.选择过滤器的策略
The strategy for our filter factory is to choose one of the filters we created based on what class was called. This will be our context. So, let’s make a few service classes that call DeserializationUtils.deserializeIntoSet(). They will be all identified by the DeserializationService interface:
我们的过滤器工厂的策略是根据调用的类来选择我们创建的过滤器之一。这就是我们的上下文。因此,让我们创建几个调用 DeserializationUtils.deserializeIntoSet() 的服务类。它们都将由 DeserializationService 接口标识:
public interface DeserializationService {
Set<ContextSpecific> process(InputStream... inputStreams);
}
public class LimitedArrayService implements DeserializationService {
@Override
public Set<ContextSpecific> process(InputStream... inputStreams) {
return DeserializationUtils.deserializeIntoSet(inputStreams);
}
}
public class LowDepthService implements DeserializationService {
// process...
}
public class SmallObjectService implements DeserializationService {
// process...
}
5.2. Filter Factory Structure
5.2.过滤器工厂结构
Our filter factory will rely on the current thread’s stack trace to check if the call is coming from a service class and which one. So let’s start with a utility method for that:
我们的过滤器工厂将依赖于当前线程的堆栈跟踪来检查调用是否来自服务类,以及来自哪个服务类。因此,让我们从实用程序方法开始:
public class ContextSpecificDeserializationFilterFactory implements BinaryOperator<ObjectInputFilter> {
private static Class<?> findInStack(Class<?> superType) {
for (StackTraceElement element : Thread.currentThread().getStackTrace()) {
try {
Class<?> subType = Class.forName(element.getClassName());
if (superType.isAssignableFrom(subType)) {
return subType;
}
} catch (ClassNotFoundException e) {
return null;
}
}
return null;
}
// ...
}
Finally, let’s override the apply() method:
最后,让我们重载 apply() 方法:
@Override
public ObjectInputFilter apply(ObjectInputFilter current, ObjectInputFilter next) {
if (current == null) {
Class<?> caller = findInStack(DeserializationService.class);
if (caller == null) {
current = FilterUtils.fallbackFilter();
} else if (caller.equals(SmallObjectService.class)) {
current = FilterUtils.safeSizeFilter(190);
} else if (caller.equals(LowDepthService.class)) {
current = FilterUtils.safeDepthFilter(2);
} else if (caller.equals(LimitedArrayService.class)) {
current = FilterUtils.safeArrayFilter(3);
}
}
return ObjectInputFilter.merge(current, next);
}
With this implementation, we:
通过这一实施方案,我们
- check if the current filter isn’t already set
- if it isn’t, we try to find if there’s a service class in the stack
- if there isn’t, we use the fallback filter
- otherwise, if the call comes from SmallObjectService, we use the safeSizeFilter() with a value of 190
- check for the other possible service classes, applying the appropriate filter
- ultimately, we merge the resulting filter with whatever is in the next filter to keep a filter that was possibly set for the ObjectOutputStream instance or via ObjectInputFilter.Config.setSerialFilter()
Note that the value for safeSizeFilter() was based on a serialized instance’s maximum expected size in bytes. Since our SampleExploit class is serialized with a larger size due to its extra content, it’s rejected on deserialization.
请注意,safeSizeFilter() 的值是基于序列化实例的最大预期大小(以字节为单位)。由于我们的 SampleExploit 类的额外内容导致其序列化后的大小较大,因此在反序列化时会被拒绝。
6. Testing Our Solution
6.测试我们的解决方案
Let’s start by setting up our tests with a few serialized Sample objects. Most importantly, we call setSerialFilterFactory() with our factory class:
首先,让我们用一些序列化的 Sample 对象来设置测试。最重要的是,我们使用工厂类调用 setSerialFilterFactory() :
static ByteArrayOutputStream serialSampleA = new ByteArrayOutputStream();
static ByteArrayOutputStream serialBigSampleA = new ByteArrayOutputStream();
static ByteArrayOutputStream serialSampleC = new ByteArrayOutputStream();
static ByteArrayOutputStream serialBigSampleC = new ByteArrayOutputStream();
@BeforeAll
static void setup() throws IOException {
ObjectInputFilter.Config.setSerialFilterFactory(new ContextSpecificDeserializationFilterFactory());
SerializationUtils.serialize(new Sample("simple"), serialSampleA);
SerializationUtils.serialize(new SampleExploit(), serialBigSampleA);
SerializationUtils.serialize(new Sample(new NestedSample(null)), serialSampleC);
SerializationUtils.serialize(new Sample(new NestedSample(new Sample("deep"))), serialBigSampleC);
}
private static ByteArrayInputStream bytes(ByteArrayOutputStream stream) {
return new ByteArrayInputStream(stream.toByteArray());
}
In this test, the resulting set contains only the “simple” object because SampleExploit was rejected, preventing the execution of maliciousCode():
在此测试中,由于 SampleExploit 被拒绝,因此结果集只包含 “简单 “对象,从而阻止了 maliciousCode() 的执行: SampleExploit 被拒绝,因此结果集只包含 “简单 “对象。
@Test
void whenSmallObjectContext_thenCorrectFilterApplied() {
Set<ContextSpecific> result = new SmallObjectService().process(
bytes(serialSampleA),
bytes(serialBigSampleA)
);
assertEquals(1, result.size());
assertEquals(
"simple", ((Sample) result.iterator().next()).getName());
}
6.1. Combined Filters
6.1.组合式过滤器
For example, when using LowDepthService, safeDepthFilter(2) is applied by our filter factory, which rejects objects with more than two levels of nesting:
例如,在使用 LowDepthService 时,我们的过滤器工厂会应用 safeDepthFilter(2),该过滤器会拒绝超过两层嵌套的对象:
@Test
void whenLowDepthContext_thenCorrectFilterApplied() {
Set<ContextSpecific> result = new LowDepthService().process(
bytes(serialSampleC),
bytes(serialBigSampleC)
);
assertEquals(1, result.size());
}
But, after modifying LowDepthService.process() to accept a custom filter:
但是,在修改 LowDepthService.process() 以接受自定义过滤器后:
public Set<ContextSpecific> process(ObjectInputFilter filter, InputStream... inputStreams) {
return DeserializationUtils.deserializeIntoSet(filter, inputStreams);
}
We can combine the safeDepthFilter() with any other filter. In this case, safeSizeFilter():
我们可以将 safeDepthFilter() 与任何其他过滤器结合使用。在这种情况下,safeSizeFilter():
@Test
void givenExtraFilter_whenCombinedContext_thenMergedFiltersApplied() {
Set<ContextSpecific> result = new LowDepthService().process(
FilterUtils.safeSizeFilter(190),
bytes(serialSampleA),
bytes(serialBigSampleA),
bytes(serialSampleC),
bytes(serialBigSampleC)
);
assertEquals(1, result.size());
}
This results in only serialSampleA being allowed.
这导致只允许使用 serialSampleA 。
7. Conclusion
7.结论
In this article, we saw Java’s latest enhancement, Context-Specific Deserialization Filter (JEP 415), in action. It introduces a dynamic and context-aware approach to filtering during deserialization operations with a filter factory. Our practical scenario showcased a service-based strategy, where different service classes were associated with specific deserialization contexts. This strategy provides a robust mechanism for developers to enhance security.
在本文中,我们看到了 Java 的最新增强功能–特定上下文反序列化过滤器(Context-Specific Deserialization Filter,JEP 415)的实际应用。它引入了一种动态和上下文感知的方法,在反序列化操作过程中使用过滤器工厂进行过滤。我们的实际应用场景展示了一种基于服务的策略,其中不同的服务类与特定的反序列化上下文相关联。
As always, the source code is available over on GitHub.
与往常一样,源代码可在 GitHub 上获取。