インナークラスなんてものわない

MITRE が国土安全保障省の支援のもと作成している Common Weakness Enumeration (CWE)。これまでのさまざまな脆弱性を分類し、ラベリングしたものです。個人的にはかなり「待ってました!」感がありますねえ。

さて、その CWE をブラウズしていて見つけたのがこのエントリ「Mobile Code: Use of Inner Class」。ついつい見落としがちな観点なのでメモ。

これはどういうことかを簡単に説明しましょう。例えば次のようなクラス Foo とその中に内包されているインナークラス Boo があったとします。

// Foo.java
package jp.ne.hatena.d.moriyoshi;

public class Foo {
        private static final class Boo {
                public static void moo() {
                        System.out.println("test");
                }
        }
}

上記のコードを見る限り、インナークラス Foo.Boo は一見 Foo の外からはアクセスできないように思えます。

では、次のように同じシグニチャを持つパッケージプライベートな Foo を作って、その中に public な Boo を定義したものを用意し、クラス Test の main() メソッドから呼んでみましょう。

// Test.java
package jp.ne.hatena.d.moriyoshi;

// 偽の Foo
class Foo {
        public static final class Boo {
                public static void moo() {}
        }
}

public class Test {
        public static void main(String[] args) {
                Foo.Boo.moo();
        }
}

これはもちろん正しくコンパイルできますし、実行もできます。

さて、ここからが奇妙なのですが、後者の手順で作成されたクラスファイル Test.class を前者の手順で作成されたクラスファイルと混合して Test を実行すると、ちゃんと実行もできて、コンソールに「test」と表示されます。

どうしてこうなってしまうのか。Java VM 上ではインナークラスの存在は定義されておらず、コンパイラエンジニアリング的に name mangling *1名前空間を分けることでエミュレートされており、かつインナークラスはパッケージプライベートとして定義されるため、実行時エラーにはならずに、同一パッケージからならアクセスできてしまうからなのです。実行時に、と書きましたが、例えば Foo.java と Test.java (から偽の Foo クラスの定義を取り除いたもの) が同じコンテキスト下でコンパイルされている場合はもちろんチェックが働きますので、コンパイル時エラーとなります。

javap コマンドを使って mangled 名でインナークラスを逆コンパイルすると、どうなっているかよくわかります。

javap -classpath . 'jp.ne.hatena.d.moriyoshi.Foo$Boo'

出力結果

Compiled from "Foo.java"
final class jp.ne.hatena.d.moriyoshi.Foo$Boo extends java.lang.Object{
    public static void moo();
}

いまさらな話の垂れ流しと思う方も多いでしょう。すみません。
それにしても、いつ何時これがセキュリティ上の脅威になるのかは謎ですね。

*1:mangling というほどでもないか