Java 17 中上下文特定的反序列化濾波器
一、簡介
在本教程中,我們將了解 Java 的新的上下文特定反序列化過濾器功能。我們將建立一個場景,然後在實踐中使用它來確定應用程式中的每種情況應使用哪些反序列化過濾器。
2. 它與 JEP 290 有何關係
Java 9 中引入了JEP 290 ,透過 JVM 範圍的過濾器以及為每個ObjectInputStream
實例定義過濾器的可能性來過濾來自外部來源的反序列化。這些過濾器根據運行時參數拒絕或允許物件被反序列化。
長期以來,人們一直在爭論反序列化不受信任資料的危險,並且幫助解決這個問題的機制也一直在改進。因此,我們現在有更多選項來動態選擇反序列化過濾器,並且創建它們也更容易。
3. JEP 415 中的ObjectInputFilter
中的新方法
為了提供有關如何以及何時定義反序列化過濾器的更多選項,JEP 415 在 Java 17 中引入了指定每次反序列化發生時調用的 JVM 範圍過濾器工廠的功能。這樣,我們的過濾解決方案就不會變得過於嚴格或過於寬泛。
此外,為了提供更多的上下文控制,有一些新方法可以簡化過濾器的建立和組合:
-
rejectFilter(Predicate<Class<?>> predicate, Status otherStatus)
:如果謂詞回傳true
則拒絕反序列化,否則回傳otherStatus
-
allowFilter(Predicate<Class<?>> predicate, Status otherStatus)
:如果謂詞回傳true, otherStatus
化 -
rejectUndecidedClass(ObjectInputFilter filter)
:映射從filter
傳遞到REJECTED
每個UNDECIDED
返回,有一些例外情況 -
merge(ObjectInputFilter filter, ObjectInputFilter anotherFilter)
:嘗試測試兩個過濾器,但在取得第一個REJECTED
狀態時傳回REJECTED
。對於anotherFilter
來說也是空安全的,返回filter
本身而不是新的組合過濾器
注意:如果正在反序列化的類別的資訊為null
, rejectFilter()
和allowFilter()
將會傳回UNDECIDED
。
4. 建立我們的場景和設置
為了說明我們的反序列化過濾器工廠的工作,我們的場景將涉及一些在其他地方序列化的 POJO,並由我們的應用程式透過幾個不同的服務類別進行反序列化。我們將使用它們來模擬可以阻止外部來源的潛在不安全反序列化的情況。最終,我們將學習如何定義參數來偵測序列化內容中的意外屬性。
讓我們從 POJO 的標記介面開始:
public interface ContextSpecific extends Serializable {}
首先,我們的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());
}
}
我們只是實作Comparable
以便稍後將實例新增到TreeSet
中。它將有助於展示如何間接執行程式碼。其次,我們將使用NestedSample
類別來更改反序列化物件的深度,我們將使用它來設定反序列化之前物件圖的深度限制:
public class NestedSample implements ContextSpecific {
private Sample optional;
public NestedSample(Sample optional) {
this.optional = optional;
}
// standard getters and setters
}
最後,讓我們建立一個簡單的漏洞利用範例以供稍後過濾。它的toString()
和compareTo()
方法中包含副作用,例如,每次我們向其中新增項目時, TreeSet
都可以間接呼叫這些方法:
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);
}
}
請注意,這個簡單的範例僅用於說明目的,並非旨在模擬現實世界的漏洞。
4.1.序列化和反序列化實用程序
為了方便以後的測試案例,讓我們創建一些實用程式來序列化和反序列化我們的物件。我們將從簡單的序列化開始:
public class SerializationUtils {
public static void serialize(Object object, OutputStream outStream) throws IOException {
try (ObjectOutputStream objStream = new ObjectOutputStream(outStream)) {
objStream.writeObject(object);
}
}
}
同樣,為了幫助我們的測試,我們將建立一個將所有非拒絕物件反序列化為一個集合的方法,以及一個可以選擇接收另一個篩選器的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;
}
}
請注意,對於我們的場景,當發生InvalidClassException
時,我們將返回null
。每當任何過濾器拒絕反序列化時都會引發此異常。這樣,我們就不會破壞deserializeIntoSet()
因為我們只對收集成功的反序列化並丟棄其他反序列化感興趣。
4.2.建立過濾器
在建造過濾器工廠之前,我們需要一些過濾器可供選擇。我們將使用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";
// ...
}
我們首先設定DEFAULT_PACKAGE_PATTERN
模式來接受「java.base」模組中的任何類別並拒絕其他任何內容。然後,我們將POJO_PACKAGE
設定為包含應用程式中需要反序列化的類別的套件。
有了這些訊息,我們就可以創建方法來作為過濾器的基礎。使用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));
}
// ...
並且,使用fallbackFilter()
,我們將建立一個限制性更強的過濾器,僅接受DEFAULT_PACKAGE_PATTERN
中的類別。它將用於我們服務類別之外的反序列化:
public static ObjectInputFilter fallbackFilter() {
return ObjectInputFilter.Config.createFilter(String.format("%s", DEFAULT_PACKAGE_PATTERN));
}
最後,讓我們編寫過濾器,用於限制讀取的位元組數、物件中的陣列大小以及反序列化物件圖的最大深度:
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);
}
完成所有設定後,我們就可以開始編寫過濾器工廠了。
5. 創建反序列化過濾器工廠
反序列化過濾器工廠允許我們根據反序列化的內容動態選擇特定的過濾器,而不是整個應用程式依賴單一過濾器。或者,每次建立ObjectInputStream
實例時都會設定不同的值。我們現在可以擁有許多特定於上下文的過濾器,並在運行時選擇或組合它們。
其機制涉及實作BinaryOperator<ObjectInputFilter>
,然後透過jdk.serialFilterFactory
JVM 屬性或透過呼叫ObjectInputFilter.Config.setSerialFilterFactory()
設定其類別名稱。該工廠是 JVM 範圍內的,只能設定一次。因此,如果它是透過 JVM 屬性設定的,則無法以程式設計方式取代它。另外,出於安全原因,不能將其設為 null。
5.1.選擇過濾器的策略
我們的過濾器工廠的策略是根據所呼叫的類別選擇我們創建的過濾器之一。這將是我們的背景。因此,讓我們建立一些呼叫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.過濾器工廠結構
我們的過濾器工廠將依賴當前執行緒的堆疊追蹤來檢查呼叫是否來自服務類別以及是哪個服務類別。那麼讓我們從一個實用方法開始:
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;
}
// ...
}
最後,讓我們重寫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);
}
透過此實施,我們:
- 檢查
current
過濾器是否尚未設定 - 如果沒有,我們嘗試尋找堆疊中是否有服務類
- 如果沒有,我們使用後備過濾器
- 否則,如果呼叫來自
SmallObjectService
,我們使用值為 190 的safeSizeFilter()
- 檢查其他可能的服務類別,套用適當的過濾器
- 最終,我們將結果過濾器與
next
過濾器中的任何內容合併,以保留可能為ObjectOutputStream
實例或透過ObjectInputFilter.Config.setSerialFilter()
設定的過濾器
請注意, safeSizeFilter()
的值是基於序列化實例的最大預期大小(以位元組為單位)。由於我們的SampleExploit
類別由於其額外內容而被序列化為更大的大小,因此在反序列化時會被拒絕。
6. 測試我們的解決方案
讓我們先使用一些序列化的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());
}
在此測試中,結果集僅包含「簡單」對象,因為SampleExploit
被拒絕,從而阻止了maliciousCode()
的執行:
@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.組合過濾器
例如,當使用LowDepthService
時,我們的過濾器工廠將應用safeDepthFilter(2)
,它會拒絕嵌套超過兩層的物件:
@Test
void whenLowDepthContext_thenCorrectFilterApplied() {
Set<ContextSpecific> result = new LowDepthService().process(
bytes(serialSampleC),
bytes(serialBigSampleC)
);
assertEquals(1, result.size());
}
但是,修改LowDepthService.process()
以接受自訂過濾器後:
public Set<ContextSpecific> process(ObjectInputFilter filter, InputStream... inputStreams) {
return DeserializationUtils.deserializeIntoSet(filter, inputStreams);
}
我們可以將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());
}
這導致僅允許serialSampleA
。
七、結論
在本文中,我們看到了 Java 的最新增強功能、上下文特定反序列化濾波器 (JEP 415) 的實際應用。它引入了一種動態且上下文感知的方法,用於在使用過濾器工廠進行反序列化操作期間進行過濾。我們的實際場景展示了基於服務的策略,其中不同的服務類別與特定的反序列化情境相關聯。該策略為開發人員提供了增強安全性的強大機制。
與往常一樣,原始碼可以在 GitHub 上取得。