Java序列化过滤器
前提
对实体类进行序列化时,控制台有如下警告:
WARN [main] - As you are using functionality that deserializes object streams, it is recommended to define the JEP-290 serial filter.Please refer to https://docs.oracle.com/pls/topic/lookup?ctx=javase15&id=GUID-8296D8E8-2B93-4B9A-856E-0A65AF9B8C66
使用反序列化对象流的功能时,建议定义JEP-290串行过滤器。
查看官方文档,对其描述如下:
为了提高序列化的安全性和健壮性,Java提供了可筛选序列化对象的传入流的序列化过滤机制。过滤器可以在反序列化输入类之前对其进行验证。
在java中,所谓的序列化即将对象的状态转换为字节流并存储到文件、数据库或发送到网络(当父类实现了标记型接口Serializable
或Externalizable
子接口,其子类也可序列化)。而反序列化则与上述过程相反,可类比为加密与解密的过程。
序列化用于许多领域,包括远程方法调用(RMI),用于进程间通信(IPC)协议的自定义RMI(例如Spring HTTP调用程序),Java管理扩展(JMX)和Java消息服务(JMS) )。当对象的序列化形式转换为该对象的副本时,该对象将反序列化。因此确保此转换的安全性很重要。反序列化的类的readObject
方法可以包含自定义代码。可序列化的类(也称为“小工具类”)可以执行任意反射操作。
创建过滤器时,可以指定接受哪些类,拒绝哪些类。也可以在反序列化期间控制对象图(object graph)
的大小和复杂度,以使对象图不会超出合理的限制。过滤器可以配置为属性,也可以通过编程实现。
除了创建过滤器之外,还可以采取以下措施来防止反序列化漏洞:
- 不要反序列化不受信任的数据。
- 使用SSL加密和验证程序之间的连接。
- 在分配之前验证字段值,包括使用readObject方法检查对象不变式。
在JEP 290中,Java序列化过滤机制的目标:
-
提供一种方法,将可以反序列化的类缩小为适合上下文的类集。
-
为反序列化期间的对象图大小和复杂度向过滤器提供指标,以验证正常的图形行为。
-
允许RMI导出的对象验证并调用期望的类。
实现序列化过滤器的方式:
- 基于模式的过滤器:此过滤器不需要修改程序,它是由一系列的模式组成,通常在属性、配置文件或命令行中定义。
- 自定义过滤器:此过滤器由ObjectInputFilter API实现。
基于模式的过滤器可以接受或拒绝特定的类,程序包或模块。他们可以限制数组大小,图形深度,总引用和流大小(通过黑名单和白名单实现)。而自定义过滤器可以特定于每个ObjectInputStream,在单个输入流或流程中的所有流上设置自定义过滤器。
基于模式的过滤器
基于模式的过滤器有一系列模式。每个模式都与流中的类名或资源限定名匹配。可以将基于类的资源限制模式组合在一个过滤器字符串中,每种模式都用分号(;)分隔。
语法
创建此过滤器时,需要注意:
-
模式之间需要使用分号隔开。
pattern1.*;pattern2.*
-
空格也是模式的一部分。
-
规则需要放在字符串的第一位。
-
模式前添加感叹号(
!
),即为拒绝模式匹配的类。!pattern1.*;pattern2.*
-
使用通配符(
*
)以一种模式表示未指定的类.
有关这些模式的语法的完整说明,请参见conf/security/java.security
文件,或参见JEP 290。
在程序中定义基于模式的过滤器
将基于模式的过滤器定义为一个程序的系统属性,系统属性取代安全属性值。jdk.serialFilter
可在命令行中定义system属性。
限制单个程序的资源使用,如下所示:
java -Djdk.serialFilter=maxarray=100000;maxdepth=20;maxrefs=500 com.example.test.Application
在所有程序中定义基于模式的过滤器
将基于模式的过滤器定义为所有程序的系统属性,系统属性取代安全属性值
- 编辑
java.security
属性文件。- JDK 9及更高版本:
$JAVA_HOME/conf/security/java.security
- JDK 8,7,6:
$JAVA_HOME/lib/security/java.security
- JDK 9及更高版本:
- 将模式添加到“
jdk.serialFilter
安全性”属性。
定义类过滤器
创建一个全局应用的基于模式的类过滤器。例如,模式可以是类名或带有通配符的包。
过滤器拒绝包(!example.somepackage.SomeClass
)中的一个类,并接受包中的所有其他类,如下所示:
jdk.serialFilter=!example.somepackage.SomeClass;example.somepackage.*;
如果要拒绝包(!example.somepackage.SomeClass
)中的所有类,需要添加(!*
):
jdk.serialFilter=!example.somepackage.SomeClass;example.somepackage.*;!*
定义资源限制过滤器
资源过滤器限制了对象图的复杂性和大小。也可以为以下参数创建过滤器,以控制每个程序的资源使用情况:
- 允许的最大数组大小。例如:
maxarray=100000;
- 图的最大深度。例如:
maxdepth=20;
- 对象之间的图形中的最大引用。例如:
maxrefs=500;
- 流中的最大字节数。例如:
maxbytes=500000;
缺点
基于模式的过滤器用于简单的接受或拒绝,这些过滤器有一些限制:
- 模式不允许基于类的不同大小的数组。
- 模式不能基于类的父类或接口来匹配类。
- 模式没有状态,并且无法选择,具体取决于流中反序列化的早期类。
自定义过滤器
自定义过滤器是在程序中指定的过滤器。它们在流程中的单个流或所有流上设置。可将自定义过滤器实现为模式,方法,lambda表达式或类。
读取序列化对象流
设置一个自定义过滤器ObjectInputStream
,如果要对每个流使用相同的过滤器,则需要设置一个进程范围的过滤器。如果ObjectInputStream
没有为其定义过滤器,则将调用全程过滤器。
在对流进行解码时,将发生以下操作:
- 对于流中的每个新对象,在实例化和反序列化对象之前将调用过滤器。
- 对于流中的每个类,将使用已解析的类来调用过滤器。流中的每个父类和接口将会分别调用它。
- 过滤器可以检查流中引用的每个类,包括要创建的对象的类,这些类的父类及其接口。
- 对于流中的每个数组,无论是原始数组,字符串数组还是对象数组,都将使用数组类和数组长度来调用过滤器。
- 对于已经从流中读取的对对象的每个引用,都会调用过滤器,以便可以检查深度,引用数和流长度。深度从1开始,并针对每个嵌套对象增加,并在每个嵌套调用返回时减小。
- 对于在流中具体编码的原语或java.lang.String实例,不会调用该过滤器。
- 筛选器返回接受,拒绝或未确定的状态。
- 如果启用了日志记录,则会记录过滤器操作。
为单个流设置自定义过滤器
当流的输入不受信任并且过滤器具有一组有限的要强制执行的类或约束时,可以在单个ObjectInputStream
上设置过滤器。例如,可以确保流仅包含数字,字符串和其他程序指定的类型。
自定义过滤器是使用设置setObjectInputFilter方法。必须先设置自定义过滤器,然后才能从流中读取对象。
在下面的示例中,setObjectInputFilter
方法与该dateTimeFilter
方法一起调用。该过滤器仅接受java.time包中的类。该 dateTimeFilter
方法在“将自定义过滤器设置为方法”的代码示例中定义。
LocalDateTime readDateTime(InputStream is) throws IOException {
try (ObjectInputStream ois = new ObjectInputStream(is)) {
ois.setObjectInputFilter(FilterClass::dateTimeFilter);
return (LocalDateTime) ois.readObject();
} catch (ClassNotFoundException ex) {
IOException ioe = new StreamCorruptedException("class missing");
ioe.initCause(ex);
throw ioe;
}
}
设置流程范围的自定义过滤器
该过滤器适用于ObjectInputStream的每次使用,除非它在特定的流上被覆盖。如果可以确定整个应用程序所需的每种类型和条件,则过滤器可以允许这些类型和条件,而拒绝其余的类型和条件。通常,过程范围的筛选器用于拒绝特定的类或程序包,或限制数组大小,对象图的深度或总大小。
使用ObjectInputFilter.Config类的方法设置一次进程范围的过滤器。过滤器可以是类,lambda表达式,方法引用或模式的实例。
ObjectInputFilter filter = ...
ObjectInputFilter.Config.setSerialFilter(filter);
在以下示例中,使用lambda表达式设置进程范围的过滤器。
ObjectInputFilter.Config.setSerialFilter(info -> info.depth() > 10 ? Status.REJECTED : Status.UNDECIDED);
在下面的示例中,通过使用方法引用来设置全过程过滤器:
ObjectInputFilter.Config.setSerialFilter(FilterClass::dateTimeFilter);
使用模式设置自定义过滤器
通过使用ObjectInputFilter.Config.createFilter
方法来创建基于模式的自定义过滤器,以方便简单使用。可以将基于模式的过滤器创建为系统属性或安全性属性。将基于模式的过滤器实现为方法或lambda表达式可提供更大的灵活性。
过滤器模式可以接受或拒绝特定的类,包,模块,并且可以限制数组大小,对象图的深度,总引用和流大小。模式不能匹配类的父类或接口。
在以下示例中,过滤器允许example.File
和拒绝example.Directory
类。
ObjectInputFilter filesOnlyFilter = ObjectInputFilter.Config.createFilter("example.File;!example.Directory");
此示例仅允许example.File
。其他所有类均被拒绝。
ObjectInputFilter filesOnlyFilter = ObjectInputFilter.Config.createFilter("example.File;!*");
将自定义过滤器设置为类
可以将自定义过滤器实现为实现java.io.ObjectInputFilter接口的类,lambda表达式或方法。
过滤器通常是无状态的,仅对输入参数执行检查。但是可以实现一个过滤器,例如,维持对checkInput
方法的调用之间的状态以对流中Artifacts的计数。
在下面的示例中,FilterNumber
该类允许作为该类实例的任何对象,Number
并拒绝所有其他对象。
class FilterNumber implements ObjectInputFilter {
public Status checkInput(FilterInfo filterInfo) {
Class<?> clazz = filterInfo.serialClass();
if (clazz != null) {
return (Number.class.isAssignableFrom(clazz)) ? Status.ALLOWED : Status.REJECTED;
}
return Status.UNDECIDED;
}
}
在示例中:
- 该
checkInput
方法接受一个ObjectInputFilter.FilterInfo
对象。对象的方法提供对要检查的类的访问,数组大小,当前深度,对现有对象的引用数以及到目前为止读取的流大小。 - 如果
serialClass
不为null,则表明正在创建一个新对象,将检查该值以查看该对象的类是否为Number
。如果是,它将被接受,否则拒绝。 - 参数的任何其他组合将返回
UNDECIDED
。反序列化继续进行,所有剩余的过滤器都会运行,直到对象被接受或拒绝为止。如果没有其他过滤器,则接受该对象。
将自定义过滤器设置为方法
使用方法引用代替内联lambda表达式。
dateTimeFilter
在下面的示例中定义的方法由“为单个流设置自定义过滤器”中的代码示例使用。
public class FilterClass {
static ObjectInputFilter.Status dateTimeFilter(ObjectInputFilter.FilterInfo info) {
Class<?> serialClass = info.serialClass();
if (serialClass != null) {
return serialClass.getPackageName().equals("java.time")
? ObjectInputFilter.Status.ALLOWED
: ObjectInputFilter.Status.REJECTED;
}
return ObjectInputFilter.Status.UNDECIDED;
}
}
示例:过滤java.base模块中的类
这个自定义过滤器(也实现为方法)仅允许在JDK的基本模块中找到的类。本示例适用于JDK 9和更高版本。
static ObjectInputFilter.Status baseFilter(ObjectInputFilter.FilterInfo info) {
Class<?> serialClass = info.serialClass();
if (serialClass != null) {
return serialClass.getModule().getName().equals("java.base")
? ObjectInputFilter.Status.ALLOWED
: ObjectInputFilter.Status.REJECTED;
}
return ObjectInputFilter.Status.UNDECIDED;
}
内置过滤器
Java远程方法调用(RMI)注册表,RMI分布式垃圾收集器和Java管理扩展(JMX)都具有JDK中包含的过滤器。
RMI注册表过滤器
注意:这些内置过滤器只能当做起点。编辑
sun.rmi.transport.dgcFilter
系统属性以配置黑名单和/或扩展白名单以为分布式垃圾收集器添加附加保护。为了保护整个程序,请将模式添加到jdk.serialFilter
全局系统属性中,以提供其他没有自定义过滤器的序列化用户的保护。
RMI分布式垃圾收集器具有一个内置的白名单过滤器,可以接受一组有限的类。它包括的情况下java.rmi.server.ObjID
,java.rmi.server.UID
,java.rmi.dgc.VMID
,和java.rmi.dgc.Lease
类。
内置过滤器包括尺寸限制:
maxarray=1000000,maxdepth=20
通过使用sun.rmi.transport.dgcFilter
模式的system属性定义过滤器来取代内置过滤器。如果过滤器接受传递给过滤器的类,或者拒绝类或大小,则不会调用内置过滤器。如果取代的过滤器不接受或拒绝任何内容,则将调用内置过滤器。
JMX过滤器
注意:这些内置过滤器只能当做起点。编辑
jmx.remote.rmi.server.serial.filter.pattern
管理属性以配置黑名单和/或扩展白名单以为JMX添加其他保护。为了保护整个应用程序,请将模式添加到jdk.serialFilter
全局系统属性中,以提供其他没有自定义过滤器的序列化用户的保护。
JMX具有一个内置过滤器,用于限制允许通过RMI作为反序列化参数发送到服务器的一组类。默认情况下,该过滤器是禁用的。要启用过滤器,请jmx.remote.rmi.server.serial.filter.pattern
使用模式定义管理属性。
该模式必须包括允许通过RMI作为参数发送到服务器的类型以及它们所依赖的所有类型javax.management.ObjectName
以及java.rmi.MarshalledObject
类型。例如,要将允许的类集限制为Open MBean类型及其依赖的类型,请在management.properties
文件中添加以下行。
com.sun.management.jmxremote.serial.filter.pattern=java.lang.*;java.math.BigInteger;java.math.BigDecimal;java.util.*;javax.management.openmbean.*;javax.management.ObjectName;java.rmi.MarshalledObject;!*
记录过滤器操作
日志记录用来记录对序列化筛选器的调用的初始化。在配置白名单和黑名单时,可以使用日志输出作为诊断工具来查看正在反序列化的内容,并确认设置。
启用日志记录后,过滤器操作将记录到java.io.serialization
记录器中。
启用序列化过滤器日志记录,编辑$JDK_HOME/conf/logging.properties
文件。
若记录被拒绝访问,则添加:
java.io.serialization.level = FINER
若记录所有过滤器结果,则添加:
java.io.serialization.level = FINEST