多くのメソッドには不変条件(invariant)が存在する。不変条件は、メソッドが何を行うか、メソッド呼出し時にそのオブジェクトに求められる状態、メソッドの呼出し完了時の状態等を定める。たとえば、剰余演算子%には以下の不変条件が成立する。
b != 0 を満たすすべての整数 a, b について、0 <= abs(a % b) < abs(b)
多くのクラスは、そのクラスの各メソッドの呼出しが完了した時のオブジェクトフィールドの状態について何らかの性質を保証する不変条件を定めている。たとえば、一度値を設定したら変更できないメンバフィールドを持つクラスは「不変(immutable)クラス」と呼ばれる。クラスが不変であるということから、これらのクラスのインスタンスに関する性質は生存期間全体に渡って維持される。
オブジェクト指向設計の基本方針の一つに、スーパークラスを拡張するサブクラスはスーパークラスの不変条件を守ることが挙げられる。残念ながら、この設計方針に従ったとしても、無害なクラスを拡張し、不変条件に違反したメソッドを持つような悪意あるクラスを攻撃者が作成することは、防ぎようがない。
たとえば、final修飾されていない不変クラスは、悪意あるサブクラスによって拡張されると、不変であるはずのオブジェクトの状態が変更されてしまう。さらに、悪意あるサブクラスのオブジェクトは、本当は可変なオブジェクトに不変であるふりをさせることができてしまう。そのような悪意あるサブクラスは、クライアントプログラムが依存する不変条件に違反し、セキュリティ上の脆弱性を生み出す。
他のコードが依存するような不変条件を持つクラスは、誤用を防ぐためにfinal宣言すべきである。特に、不変クラスはfinal宣言しなくてはならない。
スーパークラスによっては、信頼できるサブクラスによる拡張は許可しつつ、信頼できないコードによる拡張は阻止しなくてはならないものもある。そのようなスーパークラスを final 宣言すると、信頼できるサブクラスからの拡張を妨げることになるため、現実的ではない。よって、クラスの継承は慎重に設計する必要がある。
異なる保護ドメインに属する2つのクラスについて考えてみよう。一方は悪意あるクラスであり、信頼できる他方のクラスを拡張するという状況である。悪意あるサブクラスのオブジェクトが、信頼できるスーパークラスが定義するメソッド(悪意あるクラスによってオーバーライドされていない)を呼び出すことができるとしよう。このメソッドの実行は、信頼できるスーパークラスのアクセス権で行われるため、悪意あるオブジェクトは、信頼できるスーパークラスの保護ドメインの中でメソッドを実行できることになる[Gong 2003]。
一般に推奨される解決策は、スーパークラスがインスタンス化される各ポイントに、作成されるインスタンスがスーパークラスと同じ型であることをチェックするコードを置く、という方法である。そして、インスタンスの型がスーパークラスの型ではなくサブクラスの型である場合には、セキュリティマネージャによるチェックを実施し、悪意あるクラスがスーパークラスを悪用できないようにするのである。しかし、この方法は安全とも言えない。なぜなら、悪意あるクラスはファイナライザを追加し、初期化が完了していないスーパークラスのインスタンスを取得できてしまうからである。この攻撃の詳細は「OBJ11-J. コンストラクタが例外をスローする場合には細心の注意を払う」を参照のこと。
final宣言されていないクラスでは、スーパークラスのコンストラクタが終了する前にセキュリティチェックが行われることを保証するために、セキュリティマネジャのチェックを行うメソッドの呼出しを、プライベートコンストラクタの実引数として書かなければならない。この手法の例は、「OBJ11-J. コンストラクタが例外をスローする場合には細心の注意を払う」を参照。
信頼できない、final宣言されていない入力引数を受け取るメソッドは、並行する他のメソッドやスレッドが、入力オブジェクトに変更を加えるかもしれないことに注意しなくてはならない。メソッドによっては、オブジェクトのローカルコピーを作成することでこのような変更を防ごうとするかもしれないが、これでは不十分である。なぜなら、オブジェクトの浅い(shallow)コピーは可変なサブオブジェクトを参照している可能性があり、その場合、他のメソッドやスレッドが参照先のオブジェクトを変更することが可能だからである。入力オブジェクトの深い(deep)コピーを作成すれば、サブオブジェクトは変更できなくなる。しかしなお、入力オブジェクトクラスを拡張して不適切な機能を提供する可変オブジェクトをメソッドが受け取ることは、ありうる。
違反コード (BigInteger)
以下の違反コード例ではjava.math.BigIntegerクラスを拡張している。このクラスはfinal宣言されておらず、拡張することができる。これは、信頼できないクライアントから取得したBigIntegerクラスのインスタンスに対する処理を行う場合に問題になる。たとえば、悪意あるクライアントは、BigIntegerクラスのメソッドをオーバーライドし、偽のBigIntegerクラスのインスタンスを作成することができる[Bloch 2008]。
以下のコードにこのような攻撃が可能であることを示す。
BigInteger msg = new BigInteger("123"); msg = msg.modPow(exp, m); // 常に1を返す // java.math.BigInteger の偽のサブクラスを作成 class BigInteger extends java.math.BigInteger { private int value; public BigInteger(String str) { super(str); value = Integer.parseInt(str); } public void setValue(int value) { this.value = value; } @Override public java.math.BigInteger modPow(java.math.BigInteger exponent, java.math.BigInteger m) { this.value = ((int) (Math.pow(this.doubleValue(), exponent.doubleValue()))) % m.intValue(); return this; } }
無害なBigIntegerクラスとは違い、この悪意あるBigIntegerは、setValue()メソッドを持つことからも明らかなように可変クラスである。その上、悪意あるmodPow()メソッド(無害なmodPow()メソッドをオーバーライドしている)には精度の欠損をもたらす可能性がある(「NUM00-J. 整数オーバーフローを検出あるいは防止する」 「NUM12-J. 数値型の縮小変換時にデータの欠損や誤解釈を引き起こさない」 「NUM13-J. プリミティブ整数を浮動小数点数に変換する際、精度を低下させない」を参照)。このクラスのオブジェクトを受け取り、オブジェクトが不変であると決めつけているようなコードは、想定外の動作をするだろう。BigInteger.modPow()メソッドは様々な暗号アルゴリズムの実装に活用されるため、この問題は無視できない。
違反コード (セキュリティマネージャ)
以下の違反コード例は、BigIntegerクラスのコンストラクタにセキュリティマネージャによるチェックを追加している。セキュリティマネージャは、権限を持たないサブクラスによるスーパークラスのインスタンス化を検知すると、その操作を阻止する[SCG 2009]。また、「OBJ09-J. クラス名を比較するのではなくクラスを比較する」に従い、クラスの型を比較している。このチェックはBigIntegerの悪意ある拡張を防止することはできず、信頼できないコードがBigIntegerオブジェクトを作成することを防止する。これは悪意ある拡張の行われたBigIntegerのオブジェクトの作成を防止する。
public class BigInteger { public BigInteger(String str) { securityManager(); //... } // NonFinal をサブクラス化するのに必要な権限をチェック // 許可されていない場合に例外をスローする private void securityManagerCheck() { // ... } }
ファイナライザ攻撃を受ける恐れがあるため、finalでないクラスのコンストラクタから例外をスローするのは危険である(「OBJ11-J. コンストラクタが例外をスローする場合には細心の注意を払う」を参照)。
適合コード (final宣言)
以下の適合コードでは、不変であるべきBigIntegerクラスをfinal宣言することで、悪意あるサブクラスが作成されることを防止している。この方法、独自に管理するコードには有効かもしれないが、既に一般公開され過去のバージョンとの互換性が求められるJava SE API自体を変更しなくてはならないため、java.math.BigIntegerには使えない。
final class BigInteger { // ... }
適合コード (クラスの無害化)
信頼できない入力源から取得したfinalでないクラスのインスタンスは、悪意あるメソッドで上書きされている可能性があるため、注意して取り扱う必要がある。この脆弱性を防ぐには、取得したインスタンスを使用する前に、ディフェンシブコピーを作成する。以下の適合コードは、このアプローチをBigInteger引数に対して用いている。
public static BigInteger safeInstance(BigInteger val) { // java.math.BigInteger でない場合、ディフェンシブコピーを作成する if (val.getClass() != java.math.BigInteger.class) return new BigInteger(val.toByteArray()); return val; }
ディフェンシブコピーの作成については「OBJ06-J. 可変入力や可変な内部コンポーネントはディフェンシブコピーを作成する」および 「OBJ04-J. 信頼できないコードにインスタンスを安全に渡すため、可変クラスにはコピー機能を実装する」に詳しい。
適合コード (Java SE 6, パブリック/プライベートコンストラクタ)
以下の適合コードでは、プライベートコンストラクタへ渡すBoolean値を計算する際の副作用として、セキュリティマネージャのチェックを呼び出している(「OBJ11-J. コンストラクタが例外をスローする場合には細心の注意を払う」を参照)。式の評価順序に従い、プライベートコンストラクタの呼出し前に、セキュリティマネージャのチェックが行われなくてはならない。したがって、スーパークラスのコンストラクタが呼び出される前に、セキュリティマネージャのチェックが行われる。セキュリティマネージャのチェックは、作成されるオブジェクトの型が親クラスのそれかサブクラスのそれかに関わらず(信頼できるかどうかも関係なく)行われることに注意。
以下に示す解決法はファイナライザ攻撃を防止する。Java SE 6およびそれ以降のバージョンで利用することができ、java.lang.Objectコンストラクタの呼出しが終了する前に例外をスローすることで、ファイナライザの実行を阻止する[SCG 2009]。
public class BigInteger { public BigInteger(String str) { this(str, check(BigInteger.class)); } private BigInteger(String str, boolean securityManagerCheck) { // 通常のコンストラクタコード } private static boolean check(Class c) { // クラスの型を確認 if (c != BigInteger.class) { // subclassのBigIntegerに必要なパーミッションを確認 securityManagerCheck(); // 許可されていない場合にセキュリティ例外をスロー } return true; } }
違反コード (データ駆動型実行)
特権を持つコードブロックは、コードの信頼性を向上させ、かつ、セキュリティ監査を容易にするために、できるだけ簡潔に書くべきである。オーバーライド可能なメソッドの呼出しは、監査済みのクラス自体を改変しなくても特権で実行される処理内容を改変することを可能にする。その上、オーバーライド可能なメソッドの呼出しでは、実行されるコードが複数のクラスに渡るため、監査すべきコードの判別が困難になる。特権を持たないコードの実行では権限は降格されるため、悪意あるサブクラスがオーバーライド可能なメソッド呼出しを直接悪用するのは難しい。とはいえ、サブクラスを保守するプログラマは、ベースクラスの要件に違反したコードを意図せず書いてしまうかもしれない。たとえば、ベースクラスのオーバーライド可能なメソッドがスレッドセーフであったとしても、サブクラスがスレッドセーフでないメソッドでオーバーライドしてしまい、その結果、脆弱性につながることが考えられる。
以下の違反コードでは、リフレクションの仕組みを使い、特権を持つブロックの中で、オーバーライド可能なgetMethodName()メソッドを呼び出している。
public class MethodInvoker { public void invokeMethod() { AccessController.doPrivileged(new PrivilegedAction<Object>() { public Object run() { try { Class<?> thisClass = MethodInvoker.class; String methodName = getMethodName(); Method method = thisClass.getMethod(methodName, null); method.invoke(new MethodInvoker(), null); } catch (Throwable t) { // ハンドラに処理を移す } return null; } } ); } String getMethodName() { return "someMethod"; } public void someMethod() { // ... } // その他のメソッド }
サブクラスでgetMethodName()をオーバーライドし、"someMethod"以外の文字列を返すように書き換えることができる。そのようなサブクラスのオブジェクトがinvokeMethod()を呼び出すと、制御フローはsomeMethod()以外に移ってしまう。
適合コード (Final)
以下の適合コードでは、getMethodName()メソッドをfinal宣言しているため、オーバーライドできない。
final String getMethodName() { // ... }
getMethodName()メソッドのオーバーライドを禁止する別のアプローチとして、メソッドをprivate宣言する方法や、メソッドを含むクラス自体をfinal宣言する方法がある。
適合コード (ポリモーフィズムの禁止)
以下のコードは正しいgetMethodName()を呼び出しており、制御フローが変更されるのを防止している。
public void invokeMethod() { AccessController.doPrivileged(new PrivilegedAction<Object>() { public Object run() { try { Class<?> thisClass = MethodInvoker.class; String methodName = MethodInvoker.this.getMethodName(); Method method = thisClass.getMethod(methodName, null); method.invoke(new MethodInvoker(), null); } catch (Throwable t) { // ハンドラーに処理を移す } return null; } } ); }
リスク評価
クラスインスタンスの有効性を検証することなくfinal宣言されていないクラスやメソッドの継承を許可すると、悪意あるサブクラスがクラスの権限を悪用できてしまう。
ルール | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
---|---|---|---|---|---|
OBJ00-J | 中 | 高 | 中 | P12 | L1 |
関連ガイドライン
Secure Coding Guidelines for the Java Programming Language, Version 3.0 | Guideline 1-2. Limit the extensibility of classes and methods |
参考文献
[API 2006] | Class BigInteger |
[Bloch 2008] | Item 1. Consider static factory methods instead of constructors |
[Gong 2003] | Chapter 6, Enforcing Security Policy |
[Lai 2008] | Java Insecurity. Accounting for Subtleties That Can Compromise Code |
[McGraw 1999] | Chapter Seven, Rule 3. Make everything final, unless there's a good reason not to |
[Ware 2008] |
翻訳元
これは以下のページを翻訳したものです。
OBJ00-J. Limit extensibility of classes and methods with invariants to trusted subclasses only (revision 116)