概要
Java のデシリアライズ脆弱性は、2015年の Apache Commons Collections を皮切りに、WebLogic・JBoss・Jenkins など数多くのプロダクトで深刻なリモートコード実行(RCE)を引き起こしました。攻撃者が細工したバイト列を ObjectInputStream.readObject() で読み込むと、ガジェットチェーンと呼ばれるクラスの連鎖を通じて任意のコマンドが実行されます。Java 9 以降は ObjectInputFilter によるホワイトリスト方式が標準 API として提供され、許可するクラスを限定することでリスクを軽減できるようになりました。この記事では、脆弱性が発生する仕組みを概要レベルで整理したうえで、Java 8 での resolveClass オーバーライドによる手動フィルタリング、Java 9+ の ObjectInputFilter、そして Java 21 の sealed interface を組み合わせた多層防御のアプローチを、動くコードとともに解説します。
使いどころ
外部システムからバイナリデータを受け取る API で、信頼できないソースのデシリアライズを安全に処理する
レガシーシステムの Java シリアライズ通信を ObjectInputFilter で段階的に安全化する
セキュリティ監査で指摘されたデシリアライズ脆弱性に対し、ホワイトリストフィルターを導入して対策する
コード例
import java.io.*;
public class DeserializationSecurityDemo {
// シリアライズ対象の安全なクラス
static class SafeData implements Serializable {
private static final long serialVersionUID = 1L;
private final String value;
SafeData(String value) {
this.value = value;
}
@Override
public String toString() {
return "SafeData{value='" + value + "'}";
}
}
public static void main(String[] args) throws Exception {
var original = new SafeData("テストデータ");
// シリアライズ
byte[] bytes;
try (var baos = new ByteArrayOutputStream();
var oos = new ObjectOutputStream(baos)) {
oos.writeObject(original);
bytes = baos.toByteArray();
}
// Java 9+: ObjectInputFilter でホワイトリスト設定
// 許可するクラスを明示し、それ以外は !* で全拒否
String allowedClass =
DeserializationSecurityDemo.class.getName() + "$SafeData";
String filterPattern = allowedClass + ";java.lang.*;!*";
ObjectInputFilter filter =
ObjectInputFilter.Config.createFilter(filterPattern);
try (var bais = new ByteArrayInputStream(bytes);
var ois = new ObjectInputStream(bais)) {
ois.setObjectInputFilter(filter);
Object obj = ois.readObject();
System.out.println("安全なデシリアライズ成功: " + obj);
}
// maxdepth / maxarray で構造も制限可能
String strictPattern = allowedClass
+ ";java.lang.*;maxdepth=5;maxarray=100;!*";
ObjectInputFilter strictFilter =
ObjectInputFilter.Config.createFilter(strictPattern);
System.out.println("厳格フィルター: " + strictPattern);
try (var bais = new ByteArrayInputStream(bytes);
var ois = new ObjectInputStream(bais)) {
ois.setObjectInputFilter(strictFilter);
Object obj = ois.readObject();
System.out.println("厳格フィルターでも成功: " + obj);
}
}
}Version Coverage
ObjectInputFilter.Config.createFilter でフィルターパターンを文字列で指定できる。setObjectInputFilter でストリーム単位のフィルター設定が可能。
// Java 17: ObjectInputFilter でホワイトリスト設定
String pattern = "com.example.SafeData;java.lang.*;!*";
ObjectInputFilter filter =
ObjectInputFilter.Config.createFilter(pattern);
ois.setObjectInputFilter(filter);Library Comparison
注意点
ObjectInputFilter の !*(全拒否)は必ずパターンの末尾に置く。先頭に置くと全クラスが拒否されてしまう
ホワイトリストには対象クラスだけでなく java.lang.* も含める必要がある。String 等の基本型が拒否されるとデシリアライズが失敗する
ObjectInputFilter は Java 9 以降の機能。Java 8 では ObjectInputStream を継承して resolveClass をオーバーライドする手動フィルタリングが必要
フィルターを設定しても、許可したクラス自体に脆弱性がある場合は防げない。根本的な対策は Java シリアライズから JSON 等への移行
maxdepth や maxarray の制限値はアプリケーションのデータ構造に合わせて調整する。過度に厳しい値は正常なデシリアライズも失敗させる
FAQ
!* は「それ以外の全クラスを拒否する」を意味します。許可するクラスを列挙したあと、末尾に !* を置くことでホワイトリスト方式になります。
ObjectInputStream を継承して resolveClass をオーバーライドし、クラス名のホワイトリストチェックを手動で実装します。あるいは Java シリアライズ自体を JSON に置き換えるのが根本的な対策です。
リスクを大幅に軽減できますが、完全ではありません。許可したクラス自体に脆弱性がある場合は防げないため、信頼できないソースからの Java シリアライズの受信は避けるのが原則です。