何が起きたか
ステージング環境のAPIを一時的にHTTPSからHTTPに切り替えたところ、Androidアプリがクラッシュし始めました。Logcatには次のように表示されました:
java.io.IOException: Cleartext HTTP traffic to api.staging.example.com not permitted
Android 9(API 28)からこれはハードブロックになりました。警告なしでいきなりクラッシュします。Android Pie以前はHTTPでも問題なく動作していましたが、API 28以降、明示的に許可されていないドメインへのhttp://リクエストはOSによって強制的に遮断されます。実行時の回避策はありません。
これが特に問題になる3つのシナリオがあります:移行中に一時的にHTTPのみになっているバックエンド、192.168.x.xのようなローカル開発サーバー、そしてHTTPで通信し続けているサードパーティSDKです。最後のケースが最もやっかいです。そのコードは自分で書いていないのですから。
デバッグの手順
まずブロックを引き起こしている正確なドメインを特定しましょう。完全なlogcatを取得し、CLEARTEXTでフィルタリングします:
adb logcat | grep -i cleartext
次のような出力が表示されます:
W/NetworkSecurityConfig: Cleartext HTTP traffic to api.staging.example.com not permitted
これが対象ドメインです。自分のコードではない場合もあります。古いバージョンのFirebase CrashlyticsなどのアナリティクスSDKや広告SDKが原因になっていることがよくあります。
次に、network_security_config.xmlがすでに存在するか確認します:
find app/src/main/res -name "network_security_config.xml"
すでに存在している場合、誰かがネットワークセキュリティルールを設定済みで、対象ドメインがリストに含まれていないだけです。
解決策
オプション1:特定ドメインを許可する(推奨)
存在しない場合はapp/src/main/res/xml/network_security_config.xmlを作成します:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">api.staging.example.com</domain>
<domain includeSubdomains="true">192.168.1.100</domain>
</domain-config>
</network-security-config>
AndroidManifest.xmlの<application>タグ内で登録します:
<application
android:networkSecurityConfig="@xml/network_security_config"
...>
ピンポイントかつ監査可能な方法です。本番環境のHTTPSトラフィックは完全に保護されたまま、リストに記載したドメインのみHTTPアクセスが許可されます。
オプション2:デバッグビルドのみHTTPを許可する
開発時にのみHTTPが必要な場合は、許可設定をデバッグビルドに限定できます。app/src/debug/res/xml/network_security_config.xmlを作成します:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
android:networkSecurityConfig属性はデバッグマニフェスト(app/src/debug/AndroidManifest.xml)にのみ追加します。リリースビルドには影響しません。
オプション3:強制許可(本番環境には使用不可)
AndroidManifest.xmlに直接記述する方法です:
<application
android:usesCleartextTraffic="true"
...>
すべての場所でHTTPを許可します。本番環境にはリリースしないでください。Google Playにフラグを立てられますし、すべてのユーザーのトランスポートセキュリティが失われます。HTTPが本当に問題の原因かを確認するためだけに使用し、確認後は削除してください。
オプション4:HTTPSに切り替える(根本的な解決策)
自分でコントロールできるサーバーについては、HTTPSを強制してください。ローカル開発環境には自己署名証明書を取得するか、ngrokを使えば1分以内に正式なHTTPSトンネルを構築できます。まだHTTPを使っているサードパーティSDKについてはベンダーにバグ報告を。それは先方の問題です。
確認方法
リビルドしてインストールします:
./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk
アプリでネットワーク呼び出しをトリガーします。logcatからCleartext HTTP trafficの警告が消えているはずです。OkHttpを使用している場合は、ロギングインターセプターを追加してリクエストが完了するまでを確認できます:
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new HttpLoggingInterceptor().setLevel(Level.BASIC))
.build();
200レスポンスが返ってくれば修正成功です。IOExceptionが発生する場合は、そのドメインがまだホワイトリストに登録されていません。
補足情報
どのライブラリがHTTP呼び出しを行っているか特定できない場合は、デバッグ時にApplicationクラスでStrictModeを有効にしてください:
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectNetwork()
.penaltyLog()
.build());
リクエストを開始したクラスまで含めた完全なスタックトレースが記録されます。15個のSDKを手作業で調べるよりはるかに早く特定できます。
ローカル開発サーバーや企業プロキシのIPレンジを扱っている場合、ToolCraftのSubnet Calculatorを使えばCIDRレンジをすばやく確認できます。domain-configルールを書く前に192.168.x.xと10.x.x.xが同じサブネットに属するかどうか不明な場合に便利です。
学んだこと
android:usesCleartextTraffic="true"はメインマニフェストには絶対に含めないこと。どうしても必要な場合はデバッグ専用のスコープに限定する。network_security_config.xmlでのドメインごとの設定が正しいアプローチです。明示的で、コードとして確認可能で、本番トラフィックには影響しません。- リリース前に毎回
adb logcat | grep -i cleartextを実行すること。サードパーティSDKはこのエラーのサイレントな原因になりやすく、Play Storeのセキュリティレポートが届くまで気づかれないことが多いです。 - AndroidのNetwork Security Configのドキュメントは非常によく書かれています。一通り読めば、証明書ピンニング、トラストアンカー、デバッグオーバーライドなど、いずれ必要になるすべてのオプションをカバーできます。

