【翻译】2 序列化过滤
原文链接:2 Serialization Filtering
来源:Java官方文档
译者的话
由于译者的英文水平和编程水平都不高,不理解原文中的一些概念,一些句子也不知道如何翻译。对不知如何翻译的内容,译者使用了机器翻译,并在译文后面的括号中提供了原文。翻译如有错误,请参阅原文。有任何建议或意见,欢迎评论。
由于译者的疏忽,翻译的文档不是最新版,本文翻译的Java官方文档的大版本号是15。最新版比15版文档新增了一些内容,感兴趣的读者可以自己去看看。目前最新文档的链接是Serialization Filtering (oracle.com)。本段以下是正文内容。
可以使用Java序列化过滤机制帮助避免反序列漏洞。有两种过滤器:基于模式的过滤器和自定义过滤器。
主题:
解决反序列化漏洞
应用程序接受并反序列化不受信任的数据很容易受到攻击。创建过滤器可以在反序列化对象之前筛选传入的序列化对象流。
当一个对象的状态被转换为字节流时,它就被序列化了。流可以发送到一个文件、一个数据库或者通过网络传输。如果一个Java对象的类或超类实现了java.io.Serializable
接口或java.io.Externalizable
子接口,它就可以被序列化。在JDK中,序列化用于许多场合,例如远程方法调用(RMI),用于进程间通信(IPC)协议(如Spring HTTP调用器)的自定义RMI、Java管理扩展(JMX)和Java消息传递服务(JMS)。
当一个对象从序列化形式转换为该对象的副本时,它就被反序列化了。确保转换安全是重要的。反序列化时要执行代码,因为被反序列化的类的readObject
方法可以包含自定义代码。可序列化的类,也称为“小工具类”,可以执行任意反射操作,比如创建类和调用类上的方法。如果的应用程序反序列化这些类,它们可能导致拒绝服务或远程代码执行。
可以在过滤器中指定应用程序接受或拒绝的类。也可以控制反序列化时的对象图尺寸和复杂性,以便对象的图不超过合理限制。(You can control the object graph size and complexity during deserialization so that the object graph doesn’t exceed reasonable limits.)(上句中的“graph”及以后出现“graph”均不知含义,为方便查找和修正错误,本文一律翻译为“图”)有两种方式实现过滤器:配置属性和代码。
除了使用过滤器,也可以用下面的方法防止他人利用反序列化漏洞:
- 不反序列不受信任的数据。
- 使用SSL加密和验证应用程序间的连接。
- 在赋值前验证字段值,包括使用
readObject
方法检查对象不变量。(Validate field values before assignment, including checking object invariants by using thereadObject
method.)
注意:有可用于RMI的内置过滤器。但是,应该仅仅把这些内置过滤器当作起点。配置黑名单并且/或扩展白名单可以增强的应用使用RMI时的安全性。请参阅内置过滤器。
要了解更多关于这些策略的信息,请参阅Java SE安全编程指南的“序列化和反序列化”小节。
Java序列化过滤器
Java序列化过滤机制筛选传入的序列化对象流,这可以增强程序安全性的健壮性。过滤器在传入的类反序列化时执行检查。
下列是JEP 290提出的Java序列化过滤机制的目标:
- 提供一种方法,将可以反序列化的类缩小到适合上下文的类集合。(Provide a way to narrow the classes that can be deserialized down to a context-appropriate set of classes.)
- 在反序列化时,为过滤器提供图的尺寸和复杂性的指标,以验证正常的图行为。(Provide metrics to the filter for graph size and complexity during deserialization to validate normal graph behaviors.)
- 允许RMI导出的对象验证调用中期望的类。(Allow RMI-exported objects to validate the classes expected in invocations.)
下面是实现序列化过滤器的方式:
- 基于模式的过滤器不需要修改应用。它们由一系列模式组成,这些模式在属性、配置文件或命令行中定义。基于模式的过滤器可以接受或拒绝特定的类、包或模块,也可以限制数组大小、图深度、总引用数量和流大小。典型用法是把可能危害Java运行环境的类加入黑名单。基于模式的过滤器是为进程中的一个应用程序或所有应用程序定义的。(Pattern-based filters are defined for one application or all applications in a process.)(上句中的“progress”及以后出现的“progress”均不知含义,为方便查找和修正错误,本文一律翻译为“进程”)
- 自定义过滤器使用
ObjectInputFilter
API实现。它们比基于模式的过滤器的控制更加精细,因为它们可以为每个ObjectInputStream
定制。自定义过滤器可以设置在单个输入流上或进程中的所有流上。(Custom filters are set on an individual input stream or on all streams in a process.)
流中的每个新对象都会调用过滤机制。如果有多个激活的过滤器(进程级过滤器(process-wide filter)、应用过滤器或特定流过滤器)存在,只会调用最贴近当前场景的过滤器。
在大多数情况下,自定义过滤器应该检查是否设置了进程级过滤器。如果有进程级过滤器,除非它的结果是UNDECIDED
,否则自定义过滤器应该调用进程级过滤器并且使用它的结果。
从JDK 9开始,以及从8u121、7u131和6u141开始的Java CPU版本都支持序列化过滤器。
白名单和黑名单
在基于模式的过滤器和自定义过滤器都可以使用白名单和黑名单。黑/白名单能用主动或防御性的方法保护应用。
主动方法是使用白名单允许被识别的和受信任的类通过。可以在开发应用时在代码中实现白名单,或者在后面使用基于模式的过滤器实现白名单。这种方法适合只需要处理较少类的应用。通过指定允许的类、包或模块来实现白名单。
创建基于模式的过滤器
使用基于模式的过滤器时用修改应用代码。可以在配置文件中添加进程级过滤器,或者在java
命令行中添加特定应用过滤器。
基于模式的过滤器由一系列模式组成。每个模式根据类名或资源限制判断是否接受流中的对象。(Each pattern is matched against the name of a class in the stream or a resource limit.)基于类的模式和资源限制模式可以写在一个过滤器字符串中,每个模式用分号(;)分隔。
基于模式的过滤器语法
由模式组成的过滤器应遵循以下指南:
- 使用分号分隔模式。例如:
pattern1.*;pattern2.*
- 空格很重要,也是模式的一部分。
- 把限制放在过滤器字符串的首位。无论它们在字符串的什么位置,它们都会首先计算,所以把它们放在字符串的首位以强化人们对顺序的认知。如果没有限制,就会按照从左往右的用模式验证。(译注:限制包括对输入流中对象的数组长度、引用数量、图深度等的限制)
- 如果一个类匹配以
!
开头的模式,它将被拒绝。如果一个类匹配的模式开头没有!
,它将被接受。下面的过滤器拒绝了pattern1.MyClass
但是接受了pattern2.MyClass
:
!pattern1.*;pattern2.*
- 通配符(
*
)表示任意类,下面是一些示例: *
匹配任意类mypackage.*
匹配mypackage
中的所有类mypackage.**
匹配mypackage
和它的子包中的有所类text*
匹配任意以text开头的类
没有匹配任何过滤器的类将被接受。如果只想接受特定类,过滤器必须拒绝任何不匹配的东西。在类过滤器的最后使用“!*”可以拒绝所有未指定的类。
可以在conf/security/java.security
文件或JEP 290查看全部模式语法。
基于模式的过滤器限制
基于模式的过滤器仅能做到简单的接受和拒绝。这些过滤器有一些限制。例如:
- 模式无法为不同的类的数组规定不同的大小。
- 模式不能筛选类的超类或接口。
- 模式没有状态,不能根据先前从流中反序列化的类决定当前类是接受还是拒绝。
为单个应用定义基于模式的过滤器
可以为单个应用程序将基于模式的过滤器定义成系统属性。系统属性将会覆盖安全属性(Security Property)的值。(A system property supersedes a Security Property value.)
要创建一个只应用在单次Java调用的应用程序的过滤器,只需要在命令行中定义jdk.serialFilter
系统属性。
下面是一个限制单个应用程序资源使用量的示例:
java -Djdk.serialFilter=maxarray=100000;maxdepth=20;maxrefs=500 com.example.test.Application
为进程中的所有应用程序定义基于模式的过滤器(Define a Pattern-Based Filter for All Applications in a Process)
可以在安全属性定义一个基于模式的过滤器,该过滤器是为进程中所有应用定义的。系统属性将会覆盖安全属性(Security Property)的值。
- 编辑
java.security
属性文件:- Java 9和之后的版本:
$JAVA_HOME/conf/security/java.security
- JDK 8、7、6:
$JAVA_HOME/lib/security/java.security
- Java 9和之后的版本:
- 把模式添加到
jdk.serialFilter
安全属性里。
定义类过滤器
可以创建一个全局的基于模式的类过滤器。例如,一个使用通配符的类名或包名的模式。
在下例中,过滤器拒绝了一个包中的一个类(!example.somepackage.SomeClass
),并且接受该包中的所有其他类:
jdk.serialFilter=!example.somepackage.SomeClass;example.somepackage.*;
上例中的过滤器除了example.somepackage.*
中的类,接受所有其他类。要拒绝其他所有类,在末尾加上“!*”:
jdk.serialFilter=!example.somepackage.SomeClass;example.somepackage.*;!*
定义资源限制过滤器
资源过滤器限制图的复杂性和尺寸。可以用下面的参数创建过滤器限制每个应用程序的资源使用量:
- 限制数组长度。例如:
maxarray=100000
; - 限制图的深度。例如:
maxdepth=20
; - 限制图中对象引用的数量。例如:
maxrefs=500
; - 限制流的字节数。例如:
maxbytes=500000
;
创建自定义过滤器
自定义过滤器是在应用程序代码中定义的过滤器。自定义过滤器可以设置单个流上或一个进程中的所有流。可以将自定义过滤器实现为一个模式、一个方法、一个lambda表达式或一个类。
读取序列化对象流
可以在一个ObjectInputStream
上设置一个自定义过滤器,或者通过设置进程级过滤器将相同的过滤器设置到每一个流上。(You can set a custom filter on one ObjectInputStream
, or, to apply the same filter to every stream, set a process-wide filter.)。如果一个ObjectInputStream
上没有过滤器,它将会使用进程级过滤器(如果有的话)。
解码流时,执行下列操作:
- 在实例化和反序列化流中的每个新对象之前,过滤器被调用。
- 流中的每个类在被解析时,过滤器被调用。流中的每个超类和接口在被解析时都会单独调用过滤器。(The filter can examine each class referenced in the stream, including the class of objects to be created, supertypes of those classes, and their interfaces.)
- 过滤器可以检查流中引用的每个类,包括要创建的对象的类、该类的超类和它们的接口。(For each class in the stream, the filter is called with the resolved class. It is called separately for each supertype and interface in the stream.)
- 当校验数组类型和数组长度时(无论是基本类型数组、字符串数组还是对象数组),过滤器被调用。
- 每从流中读取对象的一个引用,过滤器都会检查深度、引用数和流长度。深度从1开始,每个嵌套对象会增加深度,过滤器从嵌套中返回则深度减小。
- 过滤器不会检查流中具体编码的基本类型或
java.lang.String
实例。(The filter is not called for primitives or forjava.lang.String
instances that are encoded concretely in the stream.) - 过滤器返回“接受”、“拒绝”或“未决定”状态。
- 如果启用了日志记录,则会记录过滤器操作。
如果一个对象没被过滤器拒绝,就会被接受。
在单个流上设置自定义过滤器
当流的输入不受信任并且过滤器限制类的种类或实施约束时,可以在单个ObjectInputStream
上设置过滤器。例如,可以要求流仅包含数字、字符串和其他应用程序指定的类型。
使用setObjectInputFilter
方法设置自定义过滤器。设置自定义过滤器必须在从流中读取对象之前。
在下例中,setObjectInputFilter
方法调用了dateTimeFilter
方法(In the following example, the setObjectInputFilter
method is invoked with the
method.)(译注:从代码上来看,是dateTimeFilter
setObjectInputFilter
方法将dateTimeFilter
方法设置成了过滤器)。此过滤器仅接受java.time
包中的类。dateTimeFilter
方法的定义在把方法设置为自定义过滤器的示例代码中。
1 LocalDateTime readDateTime(InputStream is) throws IOException { 2 try (ObjectInputStream ois = new ObjectInputStream(is)) { 3 ois.setObjectInputFilter(FilterClass::dateTimeFilter); 4 return (LocalDateTime) ois.readObject(); 5 } catch (ClassNotFoundException ex) { 6 IOException ioe = new StreamCorruptedException("class missing"); 7 ioe.initCause(ex); 8 throw ioe; 9 } 10 }
设置进程级的自定义过滤器
你可以设置应用于全部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
方法之间维护状态,以对流中的项目进行计数。
在下例中,FilterNumber
类仅允许Number
类的实例对象,并拒绝其他对象。
1 class FilterNumber implements ObjectInputFilter { 2 public Status checkInput(FilterInfo filterInfo) { 3 Class<?> clazz = filterInfo.serialClass(); 4 if (clazz != null) { 5 return (Number.class.isAssignableFrom(clazz)) ? Status.ALLOWED : Status.REJECTED; 6 } 7 return Status.UNDECIDED; 8 } 9 }
在上例中:
checkInput
方法的参数是一个ObjectInputFilter.FilterInfo
对象。可以用该对象的方法访问要检查的类、数组大小、当前深度、对现有对象的引用数以及到目前为止读取的流大小。- 如果
serialClass
方法的返回值不是null,则表明正在创建新对象。然后检查该对象的类是不是Number
。若是,则接受,否则拒绝。 - 参数不符合以上条件则返回
UNDECIDED
。如果还有过滤器,则继续反序列化,直到该对象被接收或拒绝。如果没有其他过滤器,则接受该对象。
将方法设置为自定义过滤器
可以用方法实现自定义过滤器。使用方法引用而不是内联的lambda表达式。
为单个流设置自定义过滤器中的示例代码使用了下例中定义的
方法。dateTimeFilter
1 public class FilterClass { 2 static ObjectInputFilter.Status dateTimeFilter(ObjectInputFilter.FilterInfo info) { 3 Class<?> serialClass = info.serialClass(); 4 if (serialClass != null) { 5 return serialClass.getPackageName().equals("java.time") 6 ? ObjectInputFilter.Status.ALLOWED 7 : ObjectInputFilter.Status.REJECTED; 8 } 9 return ObjectInputFilter.Status.UNDECIDED; 10 } 11 }
示例:只允许于java.base 模块中的类的过滤器
此自定义过滤器(作为方法实现)仅允许在JDK的基本模块中找到的类通过。此示例适用于JDK 9及更高版本。
1 static ObjectInputFilter.Status baseFilter(ObjectInputFilter.FilterInfo info) { 2 Class<?> serialClass = info.serialClass(); 3 if (serialClass != null) { 4 return serialClass.getModule().getName().equals("java.base") 5 ? ObjectInputFilter.Status.ALLOWED 6 : ObjectInputFilter.Status.REJECTED; 7 } 8 return ObjectInputFilter.Status.UNDECIDED; 9 }
内置过滤器
JDK中有适用于Java远程方法调用(RMI)注册中心、RMI分布式垃圾回收器和Java管理扩展(JMX)的过滤器。应该为RMI注册中心和RMI分布式垃圾回收器指定自己的过滤器,以增强安全性。
RMI 注册中心的过滤器
注意:只把这些内置过滤器当作起点。可以编辑
sun.rmi.registry.registryFilter
jdk.serialFilter系统属性来配置黑名单和/或扩展白名单,这能增强RMI 注册中心的安全性。若要保护整个应用程序,请将模式添加到jdk.serialFilter
全局系统属性,以增强对没有自己的自定义过滤器的其他序列化用户的保护。
RMI 注册中心具有内置的白名单过滤器,该过滤器仅允许在注册中心中绑定的对象通过。允许的对象包括java.rmi.Remotejava.lang.Number
、java.lang.reflect.Proxy
、java.rmi.server.UnicastRef
、java.rmi.activation.ActivationId
、java.rmi.server.UID
、java.rmi.server.RMIClientSocketFactory
和java.rmi.server.RMIServerSocketFactory
类的实例。
内置过滤器包括大小限制:
maxarray=1000000,maxdepth=20
请在sun.rmi.registry.registryFilter
系统属性用模式定义一个过滤器,取代内置过滤器。如果定义的过滤器要么接受传递给过滤器的类,要么拒绝类或尺寸,那么内置过滤器不会调用。如果用户定义的过滤器既不接受也不拒绝任何内容,则内置过滤器会被调用。
用于 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
系统属性用模式定义过滤器,用以取代内置过滤器。如果过滤器接受传递给过滤器的类,或者拒绝类或大小,则不会调用内置过滤器。如果取代过滤器不接受或拒绝任何内容,则调用内置过滤器。
用于 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;!*
记录过滤器行为
可以打开日志记录以记录对序列化过滤器的调用的初始化、拒绝和接受。使用日志输出作为诊断工具,以查看正在反序列化的内容,并在配置白名单和黑名单时确认的设置。(Use the log output as a diagnostic tool to see what's being deserialized, and to confirm your settings when you configure whitelists and blacklists.)
启用日志记录后,过滤器操作将记录到 java.io.serialization
序列化记录器中。
若要启用序列化过滤器的日志,请编辑$JDK_HOME/conf/logging.properties
文件。
要记录被拒绝的调用,请添加
java.io.serialization.level = FINER
若要记录所有过滤器结果,请添加
java.io.serialization.level = FINEST