最初に結論
AndroidアプリのWebViewで基本認証させようと思って検索するとよく見かけるサンプルコードについて、これではコンテンツに埋め込まれている外部サイトのオブジェクト(画像とかロギングツールとか広告オブジェクトとか)へ認証情報を流してしまうかもしれないので、注意が必要。
つまり、**認証情報を提示するホスト/URLを、きちんと制限しましょう。**という至極当然の結論。
よく見るサンプルコード
webView.setWebViewClient(new WebViewClient(){
@Override
public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
handler.proceed("ユーザ名", "パスワード");
}
});
つまり、WebViewClientクラスの「onReceivedHttpAuthRequest」をオーバーライドして、ユーザ名とパスワードを与えてあげればいい。
というもの。
まぁ、確かにそうなんだが、これでは、任意のホストの任意のレルムに認証情報を流してしまうかもしれないので、注意が必要。
実際に試してみる
環境
こんな感じ
トップの192.0.2.1のコンテンツ内部に、192.0.2.2のコンテンツ**(広告の画像とか、外部スクリプトとか)**を参照しているような場合。
192.0.2.2 側で故意に認証要求させると認証情報がそちらに漏れるかもしれない。
アプリのコード
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// これはトップのhttp://192.0.2.1:90/Auth/test.htmqだけに
// 関係する認証情報のはず、だと思ってコーディングしていると思う
String username = "sanaki";
String password = "password123!";
//
WebView webView = (WebView)findViewById(R.id.webView);
webView.setWebViewClient(new WebViewClient(){
@Override
public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
handler.proceed(username, password);
Log.i("BasicAuth", "Host=" + host + ",realm=" + realm);
}
});
// JavaScriptを有効化
webView.getSettings().setJavaScriptEnabled(true); // JavaScriptを有効にする
//
webView.loadUrl("http://192.0.2.1:90/Auth/test.htm");
}
こんな感じ。
アプリを起動すると「192.0.2.1:90」にアクセスして、そのコンテンツを表示する。というやつ。
ネットにアクセスするために
AndroidManifest.xmlに
「<uses-permission android:name="android.permission.INTERNET"/>」
と、(検証のためにhttpsにするのは面倒なので)平文のhttpをWebViewで許可するために
「android:usesCleartextTraffic="true"」
は記述している
まぁ、基本認証をしているということは、主に管理系のWebサイトだと思うけど、管理系に広告とかアクセス履歴系のやつとか埋め込んでいるのも、まぁ、たしかに想定としておかしな話かもしれんけどねぇ~
結果(まとめ)
きちんと、192.0.2.2側にも認証情報が洩れている事が、下記から確認できると思う。
結果(ログ)
Logの方には、こんな感じで、ログが出てる。
2022-01-12 15:36:06.175 18740-18740/jp.dip.rocketeer.webviewtest I/BasicAuth: Host=192.0.2.1,realm=192.0.2.1
2022-01-12 15:36:06.379 18740-18740/jp.dip.rocketeer.webviewtest I/BasicAuth: Host=192.0.2.2,realm=192.0.2.2
子フレーム側の192.0.2.2側にも認証情報が洩れているということになるだろう。
結果(192.0.2.1側{親、想定しているホスト側})
192.0.2.1 の方は、こんな感じの通信内容となっている。
RAW 通信データ
C:\>StreamRelay.NET.exe -localport 90 -remoteport 80 -remotehost 127.0.0.1 -logging
GET /Auth/test0.htm HTTP/1.1
Host: 192.0.2.1:90
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Linux; Android 11; sdk_gphone_x86 Build/RSR1.201013.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
X-Requested-With: jp.dip.rocketeer.webviewtest
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
HTTP/1.1 401 Unauthorized ← 192.0.2.1側のこれは想定している認証要求 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> </head> <div class="content-container"> </table> </table> <div class="content-container"> </fieldset> HTTP/1.1 200 OK <html> |
結果(192.0.2.2側{子、想定していないホスト側})
192.0.2.2 の方は、こんな感じの通信内容となっている。
認証情報が漏れている事が確認できる
RAW 通信データ
C:\>StreamRelay.NET.exe -localport 80 -remoteport 80 -remotehost 192.0.2.1 -logging
GET /Auth/testIn.htm HTTP/1.1
Host: 192.0.2.2
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Linux; Android 11; sdk_gphone_x86 Build/RSR1.201013.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
X-Requested-With: jp.dip.rocketeer.webviewtest
Referer: http://192.0.2.1:90/Auth/test0.htm
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
HTTP/1.1 401 Unauthorized ← 192.0.2.2側のこちらは想定されていない場面が多いと思う <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> HTTP/1.1 200 OK This is ifame page. |
まとめ
まぁ、つまり、オーバーライドする際に、hostやrealmで制限する必要があるということ。
例えば、
webView.setWebViewClient(new WebViewClient(){
String relmStr = "";
@Override
public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
// hostやrealmをチェックして、認証情報をチェックする必要がある
if(host.equals("192.0.2.1") == true){ // 例えば、ホストで制限する
if(this.relmStr.length() == 0){
// ここは一番最初の要求
this.relmStr = realm;
handler.proceed("ユーザ名", "パスワード"); // ← 想定している範囲内だけに制限することが大切
}else if(this.relmStr.equals(realm) == true){
// 2番目からは記憶しているレルムの時だけに制限する
handler.proceed("ユーザ名", "パスワード"); // ← 想定している範囲内だけに制限することが大切
}else{
handler.cancel();
}
}else{
handler.cancel();
}
}
});
こんな感じ。
192.0.2.2側ではちゃんと認証情報を漏らさずに認証エラーとなっている
RAW 通信データ
C:\>StreamRelay.NET.exe -localport 80 -remoteport 80 -remotehost 192.0.2.1 -logging
GET /Auth/testIn.htm HTTP/1.1
Host: 192.0.2.2
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Linux; Android 11; sdk_gphone_x86 Build/RSR1.201013.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
X-Requested-With: jp.dip.rocketeer.webviewtest
Referer: http://192.0.2.1:90/Auth/test0.htm
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
If-None-Match: "d424f3436d7d81:0"
If-Modified-Since: Wed, 12 Jan 2022 04:31:28 GMT
HTTP/1.1 401 Unauthorized <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-s ここから通信が途切れて、最終的に認証エラーとなっている(認証情報は漏れなくなった) |
これでも、悪意あるコンテンツと、レンタルサーバを共有しているような場合は、突破されるので、さらなる制限が必要だよ。
(第一引数のWebView viewを使ってポートとか、URLのパスとかに基づいてさらに制限をかける)
リンク
JSSECの「Androidアプリのセキュア設計・セキュアコーディングガイド」は読んでおこう。