コンストラクタがオブジェクトの構築を開始してから完了するまでの間、オブジェクトは完全には初期化されていない状態になっている。初期化が完了するまで間、オブジェクトは他のクラスからは見えてはならない。
他のクラスは、並行して実行されるスレッドを通じて、初期化が完了していないオブジェクトにアクセスするかもしれない。このルールは「TSM01-J. オブジェクトの構築時にthis参照を逸出させない」の具体例であるが、シングルスレッドプログラムのみに適用される。 マルチスレッドプログラムは「TSM03-J. 初期化が完了していないオブジェクトを公開しない」にも適合しなければならない。
変数を使用するいくつかの場面では、エラーアトミック性(failure atomicity)が要求される。典型的には、「OBJ02-J. スーパークラスに変更を加える場合、サブクラスの依存性を保つ」で紹介しているようなコンポジションと転送(composition and forwarding)に基づくアプローチにおいて、複数の異なるオブジェクトの集まりを表現する変数を使うときに要求される。エラーアトミック性が確保されていないと、初期化が完了しない結果として、オブジェクトが矛盾した状態のままにされる可能性がある。
初期化が完了していないオブジェクトの公開の問題への対応としては、以下の3つが一般的である。
- コンストラクタ中の例外. 一つのアプローチは、コンストラクタの中から例外をスローすることである。残念ながら、攻撃者はそのようなオブジェクトのインスタンスを取得できてしまう。たとえば、ファイナライザ攻撃では、セキュリティマネージャによって保護されているはずのメソッドも含めて、クラスで定義されている任意のメソッドを呼び出すことができる。
- final フィールド. 初期化されたオブジェクトを格納する変数をfinal宣言することで、初期化が完了しない問題を防止することができる。変数に格納されるオブジェクトの初期化が完了していない可能性がある場合、コンパイラは警告を発する。こうすることで、マルチスレッド環境における初期化安全性も保証される。Java言語仕様の「§17.5, finalフィールドのセマンティクス」[JLS 2005]によれば、「オブジェクトは、そのコンストラクタが完了した時点で完全に初期化されたものと考えられる。オブジェクトが完全に初期化された後のオブジェクトに対する参照のみを観測することができるスレッドは、当該オブジェクトの final フィールドの初期値を正しく観測できることが保証されている。」別の言い方をすれば、あるスレッドで実行されているコンストラクタが final フィールドを安全な値で初期化するとき、他のスレッドから、そのオブジェクトの初期化前の値を観測することはできないということである。
- 初期化フラグ. このアプローチでは、初期化されていない、あるいは初期化途中のオブジェクトは、そのような状態であることを示すフラグを持たなければならない。そのようなオブジェクトはゾンビオブジェクトと呼ばれる。この対策方法は誤りを生みやすい。なぜならば、そのようなオブジェクトに対するすべてのアクセスにおいて、初期化が正しく完了しているかどうかを最初にチェックしなければならないからである。
以下の表にこれら3つのアプローチをまとめる。
| アプローチ | 未初期化 | 初期化途中 |
| コンストラクタ中の例外 | 防止できる | 防止できない |
| final フィールド | 防止できる | 防止できる |
| 初期化フラグ | 検出できる | 検出できる |
違反コード (ファイナライザ攻撃)
Kabutz [Kabutz 2001]のコード例に基づく以下の違反コード例では、BankOperationsクラスのコンストラクタを定義している。このコンストラクタはperformSSNVerification()メソッドを使ってSSN(社会保障番号)を検証する。攻撃者は正しいSSNを知らないものと想定し、このperformSSNVerificationメソッドの実装では単純にfalseを返している。o
public class BankOperations {
public BankOperations() {
if (!performSSNVerification()) {
throw new SecurityException("Access Denied!");
}
}
private boolean performSSNVerification() {
return false; // 入力データが有効ならば true, そうでなければ false を返す.
// 攻撃者はいつでも無効な SSN を入力すると想定する
}
public void greet() {
System.out.println("Welcome user! You may now use all the features.");
}
}
public class Storage {
private static BankOperations bop;
public static void store(BankOperations bo) {
// 初期化されている場合にのみ、保存する
if (bop == null) {
if (bo == null) {
System.out.println("Invalid object!");
System.exit(1);
}
bop = bo;
}
}
}
public class UserApp {
public static void main(String[] args) {
BankOperations bo;
try {
bo = new BankOperations();
} catch (SecurityException ex) { bo = null; }
Storage.store(bo);
System.out.println("Proceed with normal logic");
}
}
SSN検証に失敗したとき、コンストラクタはSecurityExceptionをスローする。UserAppクラスはこの例外を適切にキャッチし、アクセス拒否のメッセージを表示する。しかし、このような仕組みでは、以下に示す悪意のあるプログラムによる、初期化が完了していないBankOperationsクラスのメソッド呼出しを防ぐことはできない。
攻撃の目的は、初期化途中のBankOperationsクラスのオブジェクトへの参照を入手することである。BankOperationsコンストラクタがスローしたSecurityException例外を悪意あるサブクラスがキャッチしても、新しいインスタンスにはアクセスできないので、脆弱なコードへの攻撃はそれ以上行えない。しかし、BankOperationsクラスを拡張してfinalize()メソッドをオーバーライドすることで、このコードを攻撃することができる。この方法は意図的に「MET12-J. ファイナライザは使わない」に違反している。
コンストラクタが例外をスローするとき、ガベージコレクタはオブジェクトへの参照を回収しようと待ち構えている。しかしオブジェクトは、ファイナライザがその処理を完了するまで、ガベージコレクタに回収されない。攻撃者が用意したファイナライザはthisキーワードを使って参照を入手する。このようにして攻撃者はインスタンスへの参照を盗みとり、それを基に、スーパークラスのどんなメソッドも呼び出すことができる。この攻撃はセキュリティマネージャによるチェックも回避できる。
public class Interceptor extends BankOperations {
private static Interceptor stealInstance = null;
public static Interceptor get() {
try {
new Interceptor();
} catch (Exception ex) {/* 例外を無視 */}
try {
synchronized(Interceptor.class) {
while (stealInstance == null) {
System.gc();
Interceptor.class.wait(10);
}
}
} catch(InterruptedException ex) { return null; }
return stealInstance;
}
public void finalize() {
synchronized(Interceptor.class) {
stealInstance = this;
Interceptor.class.notify();
}
System.out.println("Stole the instance in finalize of " + this);
}
}
public class AttackerApp { // クラスを実行し、制限されている機能にアクセスする
public static void main(String[] args) {
Interceptor i = Interceptor.get(); // 盗みとったインスタンス
// 本当は "Invalid Object!" と出力すべきだが、
// 盗みとったオブジェクトをコピーしてしまう
Storage.store(i);
// BankOperations クラスの任意のインスタンスメソッドを呼び出せる
i.greet();
UserApp.main(args); // 元の UserApp を呼び出す
}
}
「ERR00-J. チェック例外を抑制あるいは無視しない」および 「ERR03-J. メソッドが処理に失敗した場合はオブジェクトの状態を元に戻す」に適合することで、catchブロックの中でフィールドが適切に初期化されることを保証できる。変数を明示的にnullで初期化する場合、そのことをドキュメントに明示するべきである。そうすれば、他のプログラマやクライアントは必要に応じてnullチェックを追加することができるからである。さらに、マルチスレッド環境における初期化安全性の確保にも役立つ。
適合コード (final 宣言)
以下の適合コードでは、初期化が完了しない可能性のあるクラスをfinal宣言し、拡張されないようにしている。
public final class BankOperations {
// ...
}
適合コード (finalize()をfinal宣言する)
クラス自体をfinal宣言できない場合でも、そのクラスにfinal宣言したfinalize()メソッドを用意することで、ファイナライザ攻撃を防止することができる。
public class BankOperations {
public final void finalize() {
// 何もしない
}
}
この手法は、例外 MET12-EX1 に該当する。この例外では、クラスはファイナライザ攻撃を防ぐために空のファイナライザを使うことが認められている。
適合コード (Java SE 6, public および private コンストラクタ)
以下の適合コードは、Java SE 6およびそれ以降のバージョンに適用できる手法であり、java.lang.Objectコンストラクタが終了する前に例外がスローされた場合にファイナライザが実行されるのを防ぐ。
publicなコンストラクタの中で、privateコンストラクタの引数としてperformSSNVerification()メソッド呼出しを渡している。また、performSSNVerification()メソッドは、セキュリティチェックに失敗すると、falseを返す代わりに例外をスローする。
public class BankOperations {
public BankOperations() {
this( performSSNVerification());
}
private BankOperations(boolean secure) {
// secure の値はいつも true
// コンストラクタは自分ではセキュリティチェックを行わない
}
private static boolean performSSNVerification() {
// 有効な入力データならば true を, そうでなければ SecurityException 例外をスローする
// 攻撃者が無効な SSN を入力すると、このメソッドは例外をスローすることになる
throw new SecurityException("Invalid SSN!");
}
// ...以下、BankOperations クラスの定義の残り部分
}
コンストラクタの最初のステートメントは、スーパークラスのコンストラクタか同じクラスの別のコンストラクタの呼出しでなければならない。publicなコンストラクタでそのようなコンストラクタ呼出しが行われていない場合、スーパークラスのデフォルトコンストラクタが呼び出される。セキュリティチェックの前にスーパークラスのコンストラクタが終了してしまう場合、ファイナライザの追加と実行を許してしまうことになる。
適合コード (初期化フラグ)
以下の適合コードでは、例外をスローする代わりに初期化フラグを使って、オブジェクトの構築が完了したことを示している。初期化フラグの初期値はfalseであり、コンストラクタの実行が正常に完了したときにtrueに変更される。
class BankOperations {
private volatile boolean initialized = false;
public BankOperations() {
if (!performSSNVerification()) {
throw new SecurityException("Invalid SSN!");
}
this.initialized = true; // オブジェクト構築に成功
}
private boolean performSSNVerification() {
return false;
}
public void greet() {
if (!this.initialized) {
throw new SecurityException("Invalid SSN!");
}
System.out.println(
"Welcome user! You may now use all the features.");
}
}
initializedフラグは、構築途中のオブジェクトが持つメソッドへのアクセスを防ぐ。各メソッドは、オブジェクトの構築処理が完了しているかどうかを確認するためにinitializedフラグの値を参照しなければならないので、この方法はプログラムの実行速度に悪影響を及ぼす。また、initializedフラグを確認しないメソッドを簡単に追加できるため、コードの保守はより難しくなる。
Charlie Lai [Lai 2008] は次のように述べている。
オブジェクトの初期化が部分的にしか行われなかった場合、その内部フィールドの値は安全なデフォルト値である null などになっているであろう。そのようなオブジェクトは、信頼できない環境であっても、攻撃者にとっての利用価値は低いと考えられる。オブジェクトの初期化が部分的にしか行われていなくともセキュアな状態であると判断できるのであれば、初期化フラグを追加するべきではない。初期化フラグが必要となるのは、初期化途中のオブジェクトの状態がセキュアではないとき、あるいは、クラスの外からアクセスできるメソッドが、オブジェクト内部のフィールドを参照せずになんらかのセンシティブな操作を行う場合のみである。
違反コード (static変数)
以下の違反コード例はfinalでないstatic変数を使っている。static初期化子が使われる場合であっても、Java言語仕様は、完全な初期化と外部への安全な公開が行われることを要求しない。初期化中に例外が発生すると、このstatic変数は正しく初期化されない恐れがある。
class Trade {
private static Stock s;
static {
try {
s = new Stock();
} catch (IOException e) {
/* s を安全な状態に初期化しない */
}
}
// ...
}
適合コード (static変数をfinal宣言する)
以下の適合コードでは、Stock を final 宣言することで、この変数を安全に公開できるようにしている。
private static final Stock s;
前記の適合コードと異なり、このアプローチではnull値も許されるが、nullでない値であれば、初期化が完了したオブジェクトを参照していることが保証される。
リスク評価
初期化が完了していないオブジェクトへのアクセスを許すと、攻撃者にファイナライザの実行前あるいは実行中のオブジェクトを悪用する機会を与えてしまう。その結果、セキュリティチェックを回避される可能性がある。
| ルール | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
|---|---|---|---|---|---|
| OBJ11-J | 高 | 中 | 中 | P12 | L1 |
自動検出
このルールへの違反を自動検出することは一般に不可能である。final宣言されていないクラスのインスタンスで、そのコンストラクタが例外をスローする可能性のあるものを検出するのは簡単である。
関連する脆弱性
CVE-2008-5339はJavaの複数の脆弱性について記述している。脆弱性の1つでは、ObjectInputStream.readObject()を使ってアプレットがオブジェクトを復元(deserialize)する。ObjectInputStream.readObject()への入力は攻撃者が指定することができ、実際に読み込まれるオブジェクトはClassLoaderのシリアライズ可能なサブクラスである。このサブクラスはreadObject()メソッドを持っており、このメソッドはオブジェクトをstatic変数に格納する。その結果、オブジェクトはシリアライズを越えて生き残る。アプレットは、制約条件を与えてClassLoaderオブジェクトを構築しようとしているが、ClassLoaderはアプレットのセキュリティ制約に縛られないクラスを構築することが可能になっている。この脆弱性は「SER08-J. 特権を持ったコンテキストでは必要最小限の権限でオブジェクトを復元する」でより詳しく説明している。
関連ガイドライン
| Secure Coding Guidelines for the Java Programming Language, Version 3.0 | Guideline 1-2. Limit the extensibility of classes and methods |
| Guideline 4-3. Defend against partially initialized instances of non-final classes |
参考文献
| [API 2006] | finalize() |
| [Darwin 2004] | §9.5, The Finalize Method |
| [Flanagan 2005] | §3.3, Destroying and Finalizing Objects |
| [JLS 2005] | §12.6, Finalization of Class Instances |
| §8.3.1, Field Modifiers | |
| §17.5, Final Field Semantics | |
| [Kabutz 2001] | Issue 032: Exceptional constructors - resurrecting the dead |
| [Lai 2008] | Java Insecurity: Accounting for Subtleties That Can Compromise Code |
翻訳元
これは以下のページを翻訳したものです。
OBJ11-J. Be wary of letting constructors throw exceptions (revision 128)
