Java プログラムでは、リフレクション(reflection)を使ってプログラム自身の解析や変更を行うことができる。具体的には、フィールド変数の値を調べたり変更したりすることができる[Forman 05]、[Sun 02]。Java のリフレクションAPIには、通常アクセスできないはずのフィールドにアクセスするためのメソッドが含まれている。以下のコードは、クラス SomeClass のオブジェクト someObject が持つすべてのフィールドの名前とその値を出力する。
Field fields[] = SomeClass.class.getDeclaredFields(); for (Field field : fields) { if ( !Modifier.isPublic(field.getModifiers())) { field.setAccessible(true); } System.out.print("Field: " + field.getName()); System.out.println(", value: " + field.get(someObject)); }
フィールドに値を設定したいときは、以下のように行う。
String newValue = reader.readLine(); field.set(someObject, returnValue(newValue, field.getType()));
デフォルトのセキュリティマネージャを使っている場合、通常アクセスできないフィールドは、リフレクションの機能を使ってもアクセスできるようにはならない。もしそのようなことをしようとすると、デフォルトのセキュリティマネージャは java.security.AccessControlException 例外をスローする。しかし、java.lang.reflect.ReflectPermission のアクション suppressAccessChecks を許可すると、デフォルトの動作は抑制される。
たとえば、JVMは通常、クラスの private メンバが他のクラスのオブジェクトからアクセスされないように保護する。メソッドがリフレクションを用いてクラスのメンバにアクセスすると(つまり java.lang.reflectパッケージのAPIを使用する)、リフレクションでは同様の制限が課される。つまり、クラスの private メンバにアクセスすることのできない他のクラスは、通常、リフレクションを使ってもそれらのメンバにはアクセスできない。しかし、private メンバを持つだけでなく、これらのメンバにリフレクションを使って間接的にアクセスする public メソッドを持つクラスは、他のクラスが通常のアクセス制御を回避し、これら private メンバにリフレクションを使ってアクセスすることを可能にする。したがって、プログラマの不注意により、信頼できない呼出し元に、権限昇格につながる攻撃を許してしまう危険がある。
以下の表に、注意して使用すべき API を示す[SCG 2009]。
言語上のアクセスチェックを反映して動作するAPI |
---|
java.lang.Class.newInstance() |
java.lang.reflect.Constructor.newInstance() |
java.lang.reflect.Field.get*() |
java.lang.reflect.Field.set*() |
java.lang.reflect.Method.invoke() |
java.util.concurrent.atomic.AtomicIntegerFieldUpdater.newUpdater() |
java.util.concurrent.atomic.AtomicLongFieldUpdater.newUpdater() |
java.util.concurrent.atomic.AtomicReferenceFieldUpdater.newUpdater() |
java.lang.reflect.Field クラスの setAccessible() メソッドと getAccessible() メソッドは、JVM に言語のアクセスチェックを上書きするよう指令するために使用されるので、標準的な(そしてより制限された)セキュリティマネージャのチェックが行われる。したがってこれらのメソッドには、このルールで説明しているような脆弱性は存在しない。しかし、これらのメソッドを十分注意して使うべきであることに変わりはない。残りの set*() や get*() などのメソッドは言語上のアクセスチェックしか行われないので、脆弱である。
リフレクションの使用は、セキュリティ分析をいたずらに複雑にし、容易に脆弱性を作り込んでしまう。プログラマは可能なかぎり、リフレクション API を使わないようにすべきである。どうしてもリフレクション API が必要な場合には、特別な注意を払って使うこと。
特に、リフレクションの機能を使ってクラス、メソッド、フィールドへのアクセスを提供するのは、リフレクションを使わなくともそれらへのアクセスが可能な場合のみにしなければならない。たとえば、getter メソッドや setter メソッドを使ってアクセスできるフィールドでない限り、リフレクションを使ったアクセスを提供してはならない。
本ルールは「MET04-J. メソッドをオーバーライドあるいは隠蔽するときにアクセス範囲を広げない」に類似しているが、継承ではなくリフレクションの使用について注意を促すものである。
違反コード
以下の違反コード例では、Field オブジェクトを介したリフレクションを使うことで、private 宣言されたフィールド i と j の値を変更することができる。さらにどんなクラスも、zeroField() メソッドのリフレクション機能を使うことで、これらのフィールドの値を変更できてしまう。リフレクションを使わずにこれらのフィールドの値を変更できるのは FieldExample クラスだけである。
悪意あるコードが zeroField() メソッドに任意のフィールド名を渡せる場合、以下のような危険がある。
- 無効なフィールド名やアクセスできないフィールド名を渡されたときに例外がスローされることで、フィールド名に関する情報が漏えいする。詳しくは「ERR01-J. センシティブな情報を例外によって外部に漏えいしない」を参照。 このコード例では、メソッドの最後の部分で関連する例外をキャッチしているので、「ERR01-J. センシティブな情報を例外によって外部に漏えいしない」には適合している。
- 攻撃コードから直接はアクセスできないセンシティブなデータに、zeroField() メソッドを通じてアクセス可能になる。権限昇格につながるこのような問題を、コードレビューで発見するのは難しい。どのフィールドがアクセスされるかは、攻撃者が用いるコードが指定する文字列で決まり、レビューしているソースコードからは分からないからだ。
class FieldExample { private int i = 3; private int j = 4; public String toString() { return "FieldExample: i=" + i + ", j=" + j; } public void zeroI() { this.i = 0; } public void zeroField(String fieldName) { try { Field f = this.getClass().getDeclaredField(fieldName); // 以降のフィールド f へのアクセスは、 // zeroField() が通常のフィールド参照でアクセスできるため、 // アクセスチェックを通過する。 f.setInt(this, 0); // 適切にログをとるか例外を投げる。 } catch (NoSuchFieldException ex) { // ハンドラに通知する } catch (IllegalAccessException ex) { // ハンドラに通知する } } public static void main(String[] args) { FieldExample fe = new FieldExample(); System.out.println(fe.toString()); for (String arg : args) { fe.zeroField(arg); System.out.println(fe.toString()); } } }
適合コード (private)
リフレクションを使わざるをえない場合には、以下のコード例に示すように、リフレクション機能を呼び出すメソッドを private 宣言や final 宣言し、悪意あるコードが直接呼び出せないようにしなければならない。
class FieldExample { // ... private void zeroField(String fieldName) { // ... } }
java.lang.reflect.Field.setAccessible により、言語のアクセスチェックが上書きされた場合、zeroField() を呼び出せるメソッドは、他のクラスの private 宣言されたフィールドへのアクセスも可能になってしまうことに注意。セキュリティマネージャが他のクラスの private フィールドへのアクセスをブロックしてくれるように、ReflectPermission の suppressAccessChecks アクションを許可しないこと。
適合コード (Nonreflection)
あるクラスでフィールドへのアクセスにリフレクションを使わなければならない場合、そのようなアクセスはリフレクションを使わなくても行えるようにしておかなければならない。以下のコード例では、限定された機能を持つ setter メソッドを提供し、リフレクションを使わずにフィールドの値を0に設定できるようにしている。このような setter メソッドが他のルールやセキュリティポリシーに適合しているならば、リフレクションの使用もまたこのルールに適合していることになる。
class FieldExample { // ... public void zeroField(String fieldName) { // ... } public void zeroI() { this.i = 0; } public void zeroJ() { this.i = 0; } }
違反コード
以下の違反コード例では、Safe パッケージの外部のコードにはインスタンスの生成を許さないことをプログラマは意図していた。そのため、Trusted クラスでは、コンストラクタをパッケージプライベートにしている。しかし、Trusted クラス自体は public なので、攻撃者は Trusted.class 自体を引数として create() に渡すことで、外部のコードからパッケージプライベートなコンストラクタを呼び出せないようにするはずのアクセスチェック機能を回避できる。
package Safe; public class Trusted { Trusted() { } // パッケージプライベートなコンストラクタ public static <T> T create(Class<T> c) throws InstantiationException, IllegalAccessException { return c.newInstance(); } } package Attacker; import Safe.Trusted; public class Attack { public static void main(String[] args) throws InstantiationException, IllegalAccessException { System.out.println(Trusted.create(Trusted.class)); // 攻撃成功 } }
セキュリティマネージャ s が有効になっているとき、Class.newInstance() メソッドは、以下のいずれかの場合にセキュリティ例外をスローする。
- (a) s.checkMemberAccess(this, Member.PUBLIC) によるチェックがこのクラスの新しいインスタンスの生成を拒否したとき
- (b) 呼出し元のクラスローダが現在のクラスのクラスローダやその祖先と異なっており、s.checkPackageAccess() によるチェックがパッケージのアクセスを拒否したとき
checkMemberAccess メソッドは、呼出し元と同じクラスローダを持つ public メンバや public クラスへのアクセスを許可する。しかし、クラスローダを比較するだけでは不十分であることが多い。たとえば、すべてのアプレットは同一のクラスローダを共有している。そのため、悪質なアプレットはセキュリティチェックを通過してしまう。
適合コード (アクセス範囲の縮小)
以下の適合コードでは、create() メソッドのアクセス範囲をパッケージプライベートにすることで、パッケージ外部から呼び出されてアクセスチェックを迂回され、Trusted クラスのインスタンスを作られることを防いでいる。リフレクションを使って Trusted クラスのインスタンスを作ることができれば、(リフレクションを使うまでもなく)単純に Trusted() コンストラクタを呼び出すことができる。
package Safe; public class Trusted { Trusted() { } // パッケージプライベートなコンストラクタ static <T> T create(Class<T> c) throws InstantiationException, IllegalAccessException { return c.newInstance(); } }
適合コード (セキュリティマネージャによるチェック)
以下の適合コードでは、getConstructors() メソッドを使って、引数に渡されたクラスが public なコンストラクタを持っているかどうか調べている。public なコンストラクタを持つならば、セキュリティ上の問題はない。なぜならば、コンストラクタが public であるならば、それはすでに悪意あるコードにもアクセス可能になっているからである。public なコンストラクタを持たない場合、create() メソッドはセキュリティマネージャの checkPackageAccess() メソッドを使って、実行連鎖(execution chain)にあるすべての呼出し元が、Safe パッケージで定義されているクラスやそのメンバにアクセスするための十分なパーミッションを持っていることを確認する。
import java.beans.Beans; import java.io.IOException; package Safe; public class Trusted { Trusted() { } public static <T> T create(Class<T> c) throws InstantiationException, IllegalAccessException { if (c.getConstructors().length == 0) { // public なコンストラクタがない場合 SecurityManager sm = System.getSecurityManager(); if (sm != null) { // アクセスが許可されていなければ例外をスローする sm.checkPackageAccess("Safe"); } } return c.newInstance(); // インスタンスを返しても安全 } }
この適合コードの欠点は、クラスが getConstructors() 呼出しを許可するリフレクションのパーミッションを持っていなければならないということである。
適合コード (java.beans パッケージ)
以下の適合コードでは、java.beans.Beans API を使って、受け取った Class オブジェクトが public なコンストラクタを持っているかを調べている。
public class Trusted { Trusted() { } public static <T> T create(Class<T> c) throws IOException, ClassNotFoundException { // public なコンストラクタがある場合だけ例外なしに実行される ClassLoader cl = new SafeClassLoader(); Object b = Beans.instantiate(cl, c.getName()); return c.cast(b); } }
Beans.instantiate() メソッドの実行が成功するのは、インスタンス化されるクラスが public なコンストラクタを持っている場合だけである。それ以外の場合は、IllegalAccessException をスローする。このメソッドは、インスタンス化するクラスの名前と一緒に、クラスローダも引数として受け取る。前の適合コードとは異なり、このやり方ではリフレクションのパーミッションを必要としない。
リスク評価
直接の呼出し元に対してだけ言語のアクセスチェックを実施する API を誤用すると、データのカプセル化が無効になったり、センシティブな情報が漏えいしたり、権限昇格が行なわれる危険がある。
ルール | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
---|---|---|---|---|---|
SEC05-J | 高 | 中 | 中 | P12 | L1 |
関連ガイドライン
Secure Coding Guidelines for the Java Programming Language, Version 3.0 | Guideline 6-5. Be aware of standard APIs that perform Java language access checks against the immediate caller |
参考文献
[Chan 1999] | java.lang.reflect AccessibleObject |
翻訳元
これは以下のページを翻訳したものです。
SEC05-J. Do not use reflection to increase accessibility of classes, methods, or fields (revision 118)