OWASP カテゴリ: MASVS-NETWORK: ネットワーク通信
概要
DownloadManager は、API レベル 9 で導入されたシステム サービスです。長時間実行される HTTP ダウンロードを処理し、アプリケーションがバックグラウンド タスクとしてファイルをダウンロードできるようにします。この API は HTTP インタラクションを処理し、障害解消後、または接続変更およびシステム再起動後にダウンロードを再試行します。
DownloadManager にはセキュリティに関連する弱点があるため、Android アプリでダウンロードを管理するには安全ではありません。
(1)ダウンロード プロバイダの CVE
2018 年に、ダウンロード プロバイダで 3 つの CVE が見つかり、パッチが適用されました。それぞれの概要を以下に示します(技術的な詳細をご覧ください)。
- ダウンロード プロバイダの権限のバイパス - 権限が付与されていない場合、悪意のあるアプリはダウンロード プロバイダからすべてのエントリを取得できます。これには、ファイル名、説明、タイトル、パス、URL などの機密情報や、ダウンロードされたすべてのファイルに対する完全な読み取り/書き込み権限が含まれる可能性があります。悪意のあるアプリがバックグラウンドで実行され、すべてのダウンロードをモニタリングしてリモートで内容が漏洩する可能性があります。また、正当なリクエスト元がファイルにアクセスする前に、その場でファイルを変更することもできます。これにより、コア アプリケーションのユーザーに対してサービス拒否が発生する可能性があります(アップデートのダウンロードが不可能になるなど)。
- ダウンロード プロバイダの SQL インジェクション - SQL インジェクションの脆弱性により、権限のない悪意のあるアプリがダウンロード プロバイダからすべてのエントリを取得する可能性があります。また、権限が制限されているアプリ(
android.permission.INTERNET
など)は、別の URI からデータベースのすべてのコンテンツにアクセスすることもできます。ファイル名、説明、タイトル、パス、URL などの機密情報の取得が可能になり、権限によっては、ダウンロードしたコンテンツへのアクセスも可能になる可能性があります。 - ダウンロード プロバイダのリクエスト ヘッダー情報の漏洩 -
android.permission.INTERNET
権限が付与された悪意のあるアプリケーションは、ダウンロード プロバイダのリクエスト ヘッダー テーブルからすべてのエントリを取得できます。これらのヘッダーには、Android ブラウザや Google Chrome などのアプリから開始されたダウンロードに関するセッション Cookie や認証ヘッダーなどの機密情報が含まれる場合があります。これにより、攻撃者は、機密性の高いユーザーデータが取得された任意のプラットフォームでユーザーになりすますことができます。
(2)危険な権限
API レベル 29 より前の DownloadManager には、危険な権限(android.permission.WRITE_EXTERNAL_STORAGE
)が必要です。API レベル 29 以降では、android.permission.WRITE_EXTERNAL_STORAGE
権限は必要ありませんが、URI は、アプリが所有するディレクトリ内のパスまたはトップレベルの「ダウンロード」ディレクトリ内のパスを参照する必要があります。
(3)Uri.parse()
の使用
DownloadManager は、Uri.parse()
メソッドを使用して、リクエストされたダウンロードの場所を解析します。パフォーマンスを重視して、Uri
クラスは信頼できない入力に対してほとんど検証を行いません。
影響
DownloadManager を使用すると、外部ストレージに対する書き込み権限が悪用され、脆弱性が生じる可能性があります。android.permission.WRITE_EXTERNAL_STORAGE 権限では外部ストレージへの幅広いアクセスが許可されるため、攻撃者がファイルを変更したり、ダウンロードを変更したり、悪意のあるアプリをインストールしたり、コアアプリへのサービスを拒否したり、アプリをクラッシュさせたりすることが可能です。悪意のある行為者は、Uri.parse() に送信される内容を操作して、ユーザーに有害なファイルをダウンロードさせることもできます。
リスクの軽減
DownloadManager を使用する代わりに、HTTP クライアント(Cronet など)、プロセス スケジューラ/マネージャー、ネットワークが切断された場合に再試行を確実に行う方法を使用して、アプリ内で直接ダウンロードを設定します。ライブラリのドキュメントには、サンプルアプリへのリンクと、その実装方法に関する手順が含まれています。
アプリケーションでプロセスのスケジュール設定、バックグラウンドでのダウンロードの実行、ネットワークの切断後にダウンロードの確立を再試行する機能が必要な場合は、WorkManager
と ForegroundServices
の追加を検討してください。
以下は、Cronet を使用してダウンロードを設定するコードの例です。このコードは Cronet の Codelab から抜粋したものです。
Kotlin
override suspend fun downloadImage(url: String): ImageDownloaderResult {
val startNanoTime = System.nanoTime()
return suspendCoroutine {
cont ->
val request = engine.newUrlRequestBuilder(url, object: ReadToMemoryCronetCallback() {
override fun onSucceeded(
request: UrlRequest,
info: UrlResponseInfo,
bodyBytes: ByteArray) {
cont.resume(ImageDownloaderResult(
successful = true,
blob = bodyBytes,
latency = Duration.ofNanos(System.nanoTime() - startNanoTime),
wasCached = info.wasCached(),
downloaderRef = this@CronetImageDownloader))
}
override fun onFailed(
request: UrlRequest,
info: UrlResponseInfo,
error: CronetException
) {
Log.w(LOGGER_TAG, "Cronet download failed!", error)
cont.resume(ImageDownloaderResult(
successful = false,
blob = ByteArray(0),
latency = Duration.ZERO,
wasCached = info.wasCached(),
downloaderRef = this@CronetImageDownloader))
}
}, executor)
request.build().start()
}
}
Java
@Override
public CompletableFuture<ImageDownloaderResult> downloadImage(String url) {
long startNanoTime = System.nanoTime();
return CompletableFuture.supplyAsync(() -> {
UrlRequest.Builder requestBuilder = engine.newUrlRequestBuilder(url, new ReadToMemoryCronetCallback() {
@Override
public void onSucceeded(UrlRequest request, UrlResponseInfo info, byte[] bodyBytes) {
return ImageDownloaderResult.builder()
.successful(true)
.blob(bodyBytes)
.latency(Duration.ofNanos(System.nanoTime() - startNanoTime))
.wasCached(info.wasCached())
.downloaderRef(CronetImageDownloader.this)
.build();
}
@Override
public void onFailed(UrlRequest request, UrlResponseInfo info, CronetException error) {
Log.w(LOGGER_TAG, "Cronet download failed!", error);
return ImageDownloaderResult.builder()
.successful(false)
.blob(new byte[0])
.latency(Duration.ZERO)
.wasCached(info.wasCached())
.downloaderRef(CronetImageDownloader.this)
.build();
}
}, executor);
UrlRequest urlRequest = requestBuilder.build();
urlRequest.start();
return urlRequest.getResult();
});
}
リソース
- DownloadManager のメイン ドキュメント ページ
- DownloadManager の CVE に関する報告
- Android 権限のバイパス CVE 2018-9468
- Android ダウンロード プロバイダの SQL インジェクション CVE-2018-9493
- Android ダウンロード プロバイダの権限の回避 CVE2018-9468
- Cronet のメイン ドキュメント ページ
- アプリで Cronet を使用する手順
- Cronet の実装例
- URI のドキュメント
- ForegroundService のドキュメント
- WorkManager のドキュメント