1. 意図しないクラスのデシリアライズは行わない
セキュアコーディングスタンダードSER12-Jに該当する対策である。アプリケーションでデシリアライズすることを意図しているクラスのホワイトリストを定義しておき、デシリアライズしようとしているクラスがホワイトリストに含まれるか検証を行う。具体的には、ObjectInputStreamクラスのresolveClassメソッドをオーバーライドして、デシリアライズしようとしているクラスがホワイトリストに含まれる場合のみデシリアライズ処理を継続するようにする。第2回で解説したとおり、resolveClassメソッドはデシリアライズ対象のクラスのreadObjectメソッドの前に実行されるため、意図しないクラスのオブジェクトがデシリアライズされるのを防ぐことができる。
SER12-Jに記載されている適合コードでは、ObjectStreamClass.getName()を使用し、シリアライズデータに含まれるクラス名が意図したものか検証を行っている(resolveClassメソッドではオブジェクト情報は読みだされないため、クラス名のみの検証で問題ない)。
class WhitelistedObjectInputStream extends ObjectInputStream {
// ...
@Override
protected Class<?> resolveClass(ObjectStreamClass cls)
throws IOException, ClassNotFoundException {
if (!whitelist.contains(cls.getName())) {
throw new InvalidClassException("Unexpected serialized class",
cls.getName());
}
return super.resolveClass(cls);
}
}
以下、適合コードに基づいて実装したアプリケーションが、第1回で解説した細工したクラス(AnnotationInvocationHandler)のオブジェクトをデシリアライズした結果だ。resolveClassメソッドで例外がスローされ、意図しないクラスがデシリアライズされるのを防ぐことができる。
java.io.InvalidClassException: Unexpected serialized class; sun.reflect.annotation.AnnotationInvocationHandler
at WhitelistedObjectInputStream.resolveClass(DeserializeExample.java:15)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1613)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1518)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1774)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371)
なお、resolveClassメソッドをオーバーライドしない場合、Java標準APIによってserialVersionUIDの比較が行われるが、公開されているクラスは互換性維持などの目的でserialVersionUIDが一定であるケースが多いため、意図したクラス名かどうかを検証することが重要である。
2. 意図しないデータのデシリアライズは行わない
対策1は、デシリアライズする側の処理において、意図しないクラスのデシリアライズを防ぐアプローチであった。本対策では、デシリアライズされる側のクラスにおいて、以下の観点で意図しないデシリアライズが行われていないかを確認する。
意図しないデシリアライズ処理の呼び出しが行われていないかの検証:
→セキュアコーディングスタンダードSER13-Jに該当する対策。
デシリアライズしたオブジェクトの状態の検証:
→セキュアコーディングスタンダードSER07-Jに該当する対策。
SER13-Jは、デシリアライズ対象クラスのreadObjectメソッド内で、アプリケーションが意図しないデシリアライズ処理の呼び出しが行われていないかの検証を行うアプローチだ。サンプルコードでは、readObjectメソッド内でファイルオープンを行っており、意図しない呼び出し(アプリケーションが想定していない処理でデシリアライズされるなど)を受けた場合、多数ファイルがオープンされるなどシステムリソースが枯渇するリスクがある。この例ではDoS攻撃相当の被害が想定されるが、リフレクションの使用や、外部プロセスの起動を行っている場合などに悪意あるコード実行が行われる可能性も考えられる。
このようなリスクがある処理はデシリアライズ完了後に行うのが望ましいが、デシリアライズ時に行う必要がある場合、対策1を実施した上で、デシリアライズ対象クラスのreadObjectメソッド内で、ホワイトリストによって許可されたデシリアライズ処理かどうかを検証することをお勧めする。
SER13-Jの適合コードでは、シリアライズ対象クラスのreadObjectメソッド内の処理で、ObjectInputStreamがホワイトリストを保持しているか検証を行っている。
class OpenedFile implements Serializable {
// ...
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
boolean hasWhitelist = false;
try {
in.getClass().getDeclaredField("whitelist");
hasWhitelist = true;
} catch (ReflectiveOperationException e) {}
if (!hasWhitelist) {
throw new SecurityException("Deserialization without a whitelist is disallowed for class " +
this.getClass().getName() + ".");
}
// ...
}
}
適合コードに基づいて実装したクラスを、ホワイトリストを持たないJava標準のObjectInputStreamを使ってデシリアライズすると、例外がスローされる。
なお、SER13-Jには「SER12-JとSER13-Jのどちらかが実現されていればよい」との記載があるが、意図しないクラスのオブジェクトが生成される問題に対してはSER12-Jが必須のため、1の対策を併せて実施してほしい。
Exception in thread "main" java.lang.SecurityException: Deserialization without a whitelist is disallowed for class OpenedFile.
at OpenedFile.readObject(OpenedFile.java:40)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1017)
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:1896)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1801)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371)
また、アプリケーションが意図したデシリアライズ処理の呼び出しであっても、シリアライズデータが細工されるリスクも考慮する必要がある。
SER07-Jは、デシリアライズしたオブジェクトに対して、オブジェクトの状態があらかじめ定められた制約を満たしていることを検証することにより、意図しないオブジェクトがデシリアライズされるのを防ぐアプローチだ。デシリアライズ時にはコンストラクタが呼ばれないため、readObjectメソッドなどデシリアライズ時に実行されるメソッド内で検証を行う必要があることに注意してほしい。
SER07-Jの違反コード(Lotteryクラス)では、コンストラクタでフィールドの値に制約を設けているが、readObjectメソッドで検証を行っていないため、攻撃者がシリアライズデータのフィールド値を書き換えるなどにより、制約外のフィールド値を持ったオブジェクトをデシリアライズさせることが可能となる。適合コードでは、readObjectメソッドでフィールド値の検証を行っており、意図しないデータを持ったオブジェクトが生成されるのを防ぐことができる。
public final class Lottery implements Serializable {
// ...
private synchronized void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
ObjectInputStream.GetField fields = s.readFields();
int ticket = fields.get("ticket", 0);
if (ticket > 20000 || ticket <= 0) {
throw new InvalidObjectException("Not in range!");
}
// ...
}
}
