不安全的下載管理員

OWASP 類別: MASVS-NETWORK:網路通訊

總覽

DownloadManager 是 API 級別 9 中推出的系統服務。它可處理長時間執行的 HTTP 下載作業,並允許應用程式以背景工作模式下載檔案。該 API 的 API 會處理 HTTP 互動,並在故障或連線變更和系統重新啟動時重試下載作業。

DownloadManager 有安全性相關的弱點,因此不適合用於管理 Android 應用程式中的下載作業。

(1) 下載供應工具中的 CVE

2018 年,我們在下載提供工具中找到並修補了三個 CVE。以下概略說明各項功能 (請參閱技術詳細資料)。

  • 下載提供者權限略過 – 在未獲授權的情況下,惡意應用程式可以從下載提供者擷取所有項目,其中可能包含潛在機密資訊,例如檔案名稱、說明、標題、路徑、網址,以及所有下載檔案的完整 READ/WRITE 權限。惡意應用程式可能會在背景執行、遠端監控所有下載內容並洩漏其內容,或在合法要求者存取檔案前即時修改檔案。這可能會導致使用者無法使用核心應用程式,包括無法下載更新。
  • 下載提供者 SQL 注入攻擊:透過 SQL 注入安全漏洞,沒有權限的惡意應用程式可以從下載提供者擷取所有項目。此外,具有有限權限的應用程式 (例如 android.permission.INTERNET) 也可以透過不同的 URI 存取所有資料庫內容。可能具有敏感性的資訊,例如檔案名稱、說明、標題、路徑和網址等,也可能取得下載內容的存取權 (視權限而定)。
  • 下載提供者要求標頭資訊外洩:已授予 android.permission.INTERNET 權限的惡意應用程式,可能會從下載提供者要求標頭資料表擷取所有項目。這些標頭可能包含機密資訊,例如工作階段 Cookie 或驗證標頭,適用於從 Android 瀏覽器、Google Chrome 或其他應用程式啟動的任何下載作業。這可能會讓攻擊者在取得私密使用者資料的任何平台上冒用使用者身分。

(2) 危險權限

API 級別低於 29 的 DownloadManager 需要危險權限 – android.permission.WRITE_EXTERNAL_STORAGE。在 API 級別 29 以上版本中,您不需要 android.permission.WRITE_EXTERNAL_STORAGE 權限,但 URI 必須參照應用程式擁有的目錄中的路徑,或是頂層「Downloads」目錄中的路徑。

(3) 依賴 Uri.parse()

downloadManager 需要使用 Uri.parse() 方法剖析所要求下載的位置。為了提升效能,Uri 類別對不受信任的輸入內容幾乎不做驗證,甚至完全不做驗證。

影響

使用 downloadManager 可能導致漏洞攻擊,有心人士會入侵外部儲存空間的 WRITE 權限。由於 android.permission.WRITE_EXTERNAL_STORAGE 權限可讓應用程式存取外部儲存空間,因此攻擊者可能會悄悄修改檔案和下載內容、安裝可能含有惡意的應用程式、拒絕提供核心應用程式服務,或導致應用程式當機。惡意人士也可能操控傳送至 Uri.parse() 的內容,導致使用者下載有害檔案。

因應措施

您不應使用 downloadManager,改用 HTTP 用戶端 (例如 Cronet)、程序排程器/管理員,直接在您的應用程式中設定下載,並設法在網路遺失時重試。程式庫的說明文件包含範例應用程式的連結,以及操作說明,說明如何實作。

如果應用程式需要管理程序排程、在背景執行下載作業,或在網路中斷後重試建立下載作業,建議您考慮加入 WorkManagerForegroundServices

使用 Cronet 設定下載的程式碼範例如下,從 Cronet 程式碼研究室擷取。

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

資源