開発者の不注意や確認不足が、脆弱性につながることが理解できたと思います。
次は、サンプルコードを基に対策方法を学んでいきましょう。
脆弱性の原因
本シナリオでの脆弱性の原因は以下となります。
-
WebViewが任意のURLにアクセス可能になっている
-
JavascriptInterface経由で重要な処理を行っているメソッドを呼び出せるようになっている
対策方法の概要
実際に脆弱性のあるソースコードを確認し、修正してみましょう。
まず、「ソースコードを開く」ボタンをクリックし、「
WebViewActivity.java」を開いてください。
ソースコードを開く
修正前:
プロジェクト |
JavascriptInterface |
ソースファイル |
WebViewActivity.java |
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_sub);
uri = getIntent().getData();
webView = (WebView) findViewById(R.id.webView);
// ▼▼▼脆弱性のあるソースコード▼▼▼
// javascriptInterfaceを有効にする
webView.getSettings().setJavaScriptEnabled(true);
webView.addJavascriptInterface(this, "javascriptInterface");
// ▲▲▲脆弱性のあるソースコード▲▲▲
}
//JavascriptInterface経由で電話番号を返す
public String getLine1Number() {
return ((TelephonyManager) getSystemService(TELEPHONY_SERVICE)).getLine1Number();
}
@Override
protected Void doInBackground(String... params) {
webView.loadUrl(params[0]);
return null;
}
|
このアプリには脆弱性対策を行うべき箇所が2箇所あります。
-
URIのSchemeチェック処理追加:
25行目で受け取ったURIをチェックする処理を追加します。
悪意あるWebサイトに誘導されて細工されたJavaScriptを実行しないよう、URIのSchemeとSSL通信していることをチェックします。
さらに、WebViewが接続するURLが信用できる自社コンテンツかどうかチェックする処理を追加します。
これらの確認を行うことにより、悪意あるWebサイトに誘導された際に何らかのエラー処理を行えるようにしています。
修正例ではToastを表示し、Activityをfinish()するエラー処理を追加していますが、作成するアプリに応じて適切なエラー処理を記述するようにしましょう。
修正前ソースコードの26行目と27行目の間に、以下を追加します。
if (!"https".equals(uri.getScheme()) || !"example.jp".equals(uri.getAuthority())) {
//何らかのエラー処理を記述する
Toast.makeText(this, "不正な通信です。エラーのため、アプリを終了します。", Toast.LENGTH_LONG).show();
finish();
}
修正後のコードでは36~40行目にあたります。
-
SSL通信の追加:
新たに、SSLで通信を行うようにし、さらにSSL通信エラーを処理する処理を追加します。
今回は、プライベート認証局から払い出された、SSL接続するサーバの証明書を用いてSSL通信を行うよう実装します。サーバ証明書を用いたSSL通信の詳細については、
SSLサーバ証明書の検証不備の学習シナリオを参照してください。
SSL通信エラーが起こった場合は、アプリが中間者攻撃を受けている可能性があります。この処理を追加することにより、その攻撃の検知した際に何らかの処理を行えるようになります。
修正例では通信処理をキャンセルするようにしていますが、上記のエラー処理の場合と同様、作成するアプリに応じて適切なエラー処理を記述するようにしましょう。
修正前ソースコード61行目を以下のコードに置き換えます。
DefaultHttpClient client = new DefaultHttpClient();
try {
//Private証明書をassetsから読み込み
KeyStore ks = KeyStoreUtil.getEmptyKeyStore();
KeyStoreUtil.loadX509Certificate(ks,
mContext.getResources().getAssets().open("cacert.crt"));
Scheme sch = new Scheme("https", new SSLSocketFactory(ks), 443);
client.getConnectionManager().getSchemeRegistry().register(sch);
HttpGet request = new HttpGet(params[0]);
HttpResponse response = client.execute(request);
webView.loadData(EntityUtils.toString(response.getEntity()), "text/html", "UTF-8");
} catch (SSLException e) {
this.cancel(true);
} catch (Exception e) {
}finally {
client.getConnectionManager().shutdown();
}
修正後のコードでは78~98行目にあたります。
今回の対策は、悪意のあるコンテンツを読み込まないようにするというものです。そのためJavascriptInterface経由で公開している処理については変更を加えていません。
-
ライブラリのインポート
上記修正の際に、下記のライブラリをインポートする必要があります。
修正中の
WebViewActivity.javaの先頭にあるimport文の下に、以下のimport文を追加してください。
-
import org.apache.http.HttpResponse;
-
import org.apache.http.client.methods.HttpGet;
-
import org.apache.http.conn.scheme.Scheme;
-
import org.apache.http.conn.ssl.SSLSocketFactory;
-
import org.apache.http.impl.client.DefaultHttpClient;
-
import org.apache.http.util.EntityUtils;
-
import javax.net.ssl.SSLException;
-
import java.security.KeyStore;
修正後:
プロジェクト |
JavascriptInterface |
ソースファイル |
WebViewActivity.java |
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_sub);
uri = getIntent().getData();
// ▼▼▼脆弱性を修正したソースコード▼▼▼
if (!"https".equals(uri.getScheme()) || !"example.jp".equals(uri.getAuthority())) {
//何らかのエラー処理を記述する
Toast.makeText(this, "不正な通信です。エラーのため、アプリを終了します。", Toast.LENGTH_LONG).show();
finish();
}
// ▲▲▲脆弱性を修正したソースコード▲▲▲
webView = (WebView) findViewById(R.id.webView);
webView.getSettings().setJavaScriptEnabled(true);
webView.addJavascriptInterface(this, "javascriptInterface");
}
protected Void doInBackground(String... params) {
// ▼▼▼脆弱性を修正したソースコード▼▼▼
DefaultHttpClient client = new DefaultHttpClient();
try {
//Private証明書をassetsから読み込み
KeyStore ks = KeyStoreUtil.getEmptyKeyStore();
KeyStoreUtil.loadX509Certificate(ks,
mContext.getResources().getAssets().open("cacert.crt"));
Scheme sch = new Scheme("https", new SSLSocketFactory(ks), 443);
client.getConnectionManager().getSchemeRegistry().register(sch);
HttpGet request = new HttpGet(params[0]);
HttpResponse response = client.execute(request);
webView.loadData(EntityUtils.toString(response.getEntity()), "text/html", "UTF-8");
} catch (SSLException e) {
this.cancel(true);
} catch (Exception e) {
}finally {
client.getConnectionManager().shutdown();
}
// ▲▲▲脆弱性を修正したソースコード▲▲▲
return null;
}
|
このアプリはサンプルアプリのため、アプリ内のassetsフォルダからHTMLファイルを読み込むよう実装しています。
そのため、脆弱性対策後に、SSL通信以外の通信と、自社コンテンツのサーバへの接続以外をエラーとした場合、正常な起動であってもエラーと認識されるため、アプリは起動しません。
本来の証明書の一致するインターネット上の自社コンテンツを表示するアプリであれば、正常に自社コンテンツが表示されます。
対策のまとめ
「JavascriptInterfaceの理解不足」による脆弱性について、対策は以下のとおりです。
- 対策
Android OSのバージョンに応じて、下記のいずれかの対策を実施する。
-
JavascriptInterfaceを使わない
-
JavascriptInterfaceで公開するJavaメソッドを制限し、かつ重要な処理を行わない
-
JavascriptInterfaceを有効にしたWebViewがアクセスするURLを制限する
- 修正方法
-
minSdkVersionが16以下の場合
-
WebViewが任意のURLを表示する場合はJavascriptInterfaceを有効にしない
-
WebViewが自社コンテンツのみを表示する場合はURLの制限を行い、かつ、SSL通信を用いる
-
minSdkVersionが17以上の場合
-
WebViewが任意のURLを表示する場合はJavascriptInterfaceに設定したメソッドで重要な処理を行わない
-
WebViewが自社コンテンツのみを表示する場合はURLの制限を行い、かつ、SSL通信を用いる
-
開発者がアプリ内でJavascriptInterfaceを有効にするかどうかの判断
minSdkVersionとWebViewがアクセスするURLの種別により、アプリ内でJavascriptInterfaceを有効にしてよいかどうか表にまとめると以下のようになります。
minSdkVersion
|
WebViewがアクセスするURL
|
任意
|
自社コンテンツのみ
|
16(Android4.1)以下
|
不可
|
可
|
17(Android4.2)以上
|
条件付きで可
|
可
|
JavascriptInterfaceを有効にしてよい場合であっても、下記に基づき適切な対策を行う必要があります。
-
minSdkVersionが16以下のアプリの場合(Android4.1以下のバージョンも対象とする場合)
minSdkVersionが16以下のアプリではJavascriptInterface経由で任意のJavaのメソッドが実行されてしまいます。
アプリでJavascriptInterfaceを有効にする場合には、悪意ある者が用意した想定外のコンテンツにアクセスしないように、WebViewがアクセスするURLを制限する必要があります。
-
minSdkVersionが17以上のアプリの場合(Android4.2以上のバージョンを対象とする場合)
アノテーションで呼び出せるメソッドを制限できるようにセキュリティが強化されているため、対策を行いつつJavascriptInterfaceを有効にすることができます。
アノテーションのついていないメソッドはJavaScriptから呼び出されませんが、アノテーションのついているメソッドは呼び出すことができます。
WebViewが任意のURLにアクセスする場合は、細工されたJavaScriptから呼び出され、メソッドが悪用された場合であっても問題が無い、重要な処理をしないメソッドに公開を限定する必要があります。