愛と勇気と缶ビール

ふしぎとぼくらはなにをしたらよいか

AndroidのWebView#addJavascriptInterfaceは基本使わない方がいい、っていう話

基礎知識

Androidアプリケーションで埋め込みブラウザのようなものを実現するためのViewコンポーネントであるandroid.webkit.WebViewには次のようなシグネチャでaddJavascriptIntefaceというメソッドが生えています。

http://developer.android.com/reference/android/webkit/WebView.html#addJavascriptInterface(java.lang.Object,%20java.lang.String)

これは、簡単に言うとJavaのオブジェクトをWebView内でロードされたJavaScriptから叩けるようになる、というものです。例えば

   class JSInterface {
        private Context context;

        public JSInterface(Context ctx) {
            this.context = ctx;
        }

        void sendString(String msg) {
            Log.d("message from JavaScript: " + msg);
        }
    }

    (中略)

    webView.addJavascriptInterface(new JSInterface(this), "android");

このような呼び出しをJava側で行った場合、WebView内のwindowでロードされたJS側からはwindow.androidというオブジェクトが見えるようになります。
android.sendString("Hello, world!"); という風にJS側からコールしてやると、logcatに "message from JavaScript: Hello, world!" と表示されます。
これでめでたく、JavaScript -> Java という方向の通信が行えるようになりました。

それじゃ、Java側からJavaScriptに処理(そして値)を戻したい場合はどうするの?ということですが以下のblog記事にWebView#loadUrlを使ったやり方が書いてあります。

http://d.hatena.ne.jp/glass-_-onion/20110302/1299071501

addJavascriptInterfaceにおけるJS -> Java間の型変換

addJavascriptInterfaceの利用例を示したblog記事などでは、文字列を渡している場合がほとんどです。というか僕は文字列以外のものを渡している例を見たことがないです。なので、JavaScriptの値のうちどういうものがaddJavascriptInterface経由で受け渡せるかを調べました。JavaScriptにおける「型」については、typeof演算子で取れる値をベースにします。

大体以下のような型変換が行われます。

  • JavaScriptにおけるstring -> JavaのStringで受けることが可能。
  • JavaScriptにおけるnumber -> JavaのStringで受けることが可能。
  • JavaScriptにおけるnumber -> Javaのintで受けることが可能。1.24などを渡した場合は小数点以下が切り捨てられる
  • JavaScriptにおけるnumber -> Javaのdoubleで受けることが可能。1などの整数を渡した場合は1.0になる
  • JavaScriptにおけるboolean -> Javaのbooleanで受けることが可能。
  • JavaScriptにおけるobject -> Map, HashMap, TreeMapなどを試したが受けられなかった
  • JavaScriptにおけるobject [1, 2, 3] -> int[] で受けることが可能。stringを混ぜた場合、その値は0になる。
  • JavaScriptにおけるobject ["foo", "bar", "baz"] -> String[] で受けることが可能。numberを混ぜた場合、その値はnullになる。

不完全なリストではありますが、空気感は分かると思います。僕のJavaについての知識が乏しいのもありますが、いわゆるJavaScriptにおける単純なobject = { "foo": 1, "bar", 2 } のような値をJava側で受ける方法はわかりませんでした。

addJavascriptInterfaceで公開されたJava objectの、JS objectとしての挙動

addJavascriptInterfaceを通じて公開されたobjectは、以下のような特徴を持ちます。ここでは、"android"という名前でglobalに公開されている(window.androidとしてアクセスできる)ものとします。

  • delete window.android; で削除が可能。
  • window._android = window.android; のようにコピーが可能。
  • 上記の2つを組み合わせて、違う名前にすることが可能。
  • Java objectに元々生えているメソッドについては、deleteや上書きが無効。
  • ただし、property descriptorsによる制御ではない。
  • JS側でtoString() を呼ぶと、"{packagename}.{classname}@{hashnumber}" という文字列が返ってくる。例えば、"com.foo.bar.JSInterface@12345678" のような。(これJavaのtoString()のデフォルト挙動だっけか)
  • addJavascriptInterfaceを読んだWebView内の全てのwindowについて、window.androidは利用可能になる。例えばiframe内のwindowなど。

下から二番目の挙動を利用することで、例えば以下のようなコードを用いてaddJavascriptInterfaceで公開されたobjectを探索することができます。

var k, str, jsInterface;
for ( k in window ) {
    if ( typeof window[k] === "object" && window[k] !== null ) {
        str = window[k].toString();
        if ( str.match(/@\d+/) && ! str.match(/webkit/) ) {
            jsInterface = window[k];
        }
    }
}

段々雲行きが怪しくなってきましたね。

WebView#addJavascriptInterfaceで公開されたオブジェクト経由のreflection

下の記事にもあるように、addJavascriptInterfaceで公開されたJava objectからJavaのreflection APIを叩いて様々なことが出来てしまいます。

http://www.kanasansoft.com/weblab/2012/04/webview_addjavascriptinterface_of_android_is_dangerous.html

この記事の意味するところは、「android.permission.READ_PHONE_STATEなどの個人情報を抜ける権限を持ったアプリ内のWebViewで、Androidにおける Context class (Activity, Service, Applicationなどの親となる重要なclass) またはそのサブクラスのインスタンスが公開されてしまうとアウト」ということです。

この記事の例ではAndroidのActivity(のサブクラス)を公開しているので、「流石にActivityをそのまま公開するとかしないわー、俺のは大丈夫だわー」とかミサワ風に安心してしまった人もいるかと思います。しかし実際には、Activity, Service, Applicationやそれらのサブクラスを公開しない、という対策だけでは不十分です。

例えば、今僕が「android javascript」というクエリでGoogle検索してたまたま一番上に来た↓の記事

http://www.adamrocker.com/blog/172/javascript_android_bridge.html

にもあるように、addJavascriptInterfaceで公開するobjectのprivate fieldにContextを持ってしまっている場合。Java側での可視性がprivateになっているので一見安全かのようですが、このようにJavaScript側からreflectionを使って (jsInterfaceは既に上の方法を用いて探索済とします)

var klass = jsInterface.getClass();
var field = klass.getDeclaredField("con");
field.setAccessible(true);
var context = field.get(jsInterface);

fieldの可視性を変更した上で、簡単にContextが取得できてしまいます。

この記事に限らず、インターネット上に公開されているaddJavascriptInterfaceの利用例では公開されるオブジェクトのfieldとしてContextを持たせている例が非常に多いです。まるで情報のレプリケーションが行われているようです。また、AndroidAPIにはContextを渡すものが少なからず存在するため、現実のAndroidアプリ開発において自分で定義したclassのfieldにContextを持たせる、というのは (それが推奨された行為であるかは別として) よくあるパターンです。

このパターン以外でも、要はreflection経由で辿れる所にContextのインスタンスを置いてしまった時点でアウト、ということです。僕のJava力は低いため、reflectionを上手くつかいこなせる人にかかると、更に容易な方法があるかもしれません。

Java側での"con"とか"context"とか"m_context"とかのfield名をどうやって知るの!という疑問もあるかと思います。reflection APIのClass#getDeclaredFieldsはJS側からうまく叩くことが出来ないようで、reflectionを使ってfieldを探索することは出来ません。しかし、実はJS側からfield名を探索する必要はないのです。

Android本体からアプリのapkをぶっこ抜き、apktoolを用いてapkをバラし、.smailファイルを解析してどこかにContextをお漏らししてないか調べるのは非常に簡単です。Androidにはproguardというソースコード難読化ツールがありますが、これを使った場合でもユーザの定義したclass名やfield名などのシンボルが"a"や"b"になるだけなので、解析の簡単さはほぼ変わりありません。

つまるところの、WebView#addJavascriptInterfaceの危険性

以下の条件さえ揃えば、第三者のJSからAndroid端末内の個人情報を比較的簡単に取得できてしまいます。

  • 該当のAndroidアプリのapkを入手、解析可能。
  • 該当のAndroidアプリが、READ_PHONE_STATEなどの個人情報にアクセスできる権限を持っている。
  • 該当のAndroidアプリが、WebView#addJavascriptInterfaceを使って、reflection経由(または他の方法)でContextを取得できるようなobjectを公開している。
  • 該当のAndroidアプリのWebView内で任意のサイトに移動できる、XSS脆弱性のあるサイトを表示している、第三者のページをiframeなどで開くことができる。要は任意のJSコードを実行できる。

addJavascriptInterfaceはこのようなものであるため、上記の条件をちゃんと回避できる、という自信のある方以外は基本「使わない」という選択肢を取ることをおすすめします。この記事を読んで「よくわからないんだけど」という方も同様です。JS <-> Java間通信の代替手段としては、インタフェースは貧弱になりますが安全なWebChromeClient#onJsAlertやWebChromeClient#onJsPromptを用いるとよいのではないかと。


Android Security  安全なアプリケーションを作成するために

Android Security  安全なアプリケーションを作成するために