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

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();
    });
}

リソース