安全でないダウンロード マネージャー

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 など)、プロセス スケジューラ/マネージャー、ネットワークが切断された場合に再試行を確実に行う方法を使用して、アプリ内で直接ダウンロードを設定します。ライブラリのドキュメントには、サンプルアプリへのリンクと、その実装方法に関する手順が含まれています。

アプリケーションでプロセスのスケジュール設定、バックグラウンドでのダウンロードの実行、ネットワークの切断後にダウンロードの確立を再試行する機能が必要な場合は、WorkManagerForegroundServices の追加を検討してください。

以下は、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();
    });
}

リソース