OWASP 類別: MASVS-NETWORK:網路通訊
總覽
DownloadManager 是在 API 級別 9 中導入的系統服務。可處理長時間執行的 HTTP 下載作業,並允許應用程式將檔案下載作業做為背景工作執行。這個 API 會處理 HTTP 互動,並在下載失敗、連線變更和系統重新啟動後重試下載。
DownloadManager 有安全相關的弱點,因此不適合在 Android 應用程式中管理下載作業。
(1) 下載供應商中的 CVE
2018 年,我們在 DownloadProvider 中發現並修復了三項 CVE。以下是各項功能的摘要 (請參閱技術詳細資料)。
- 下載供應商權限繞過 - 如果沒有授予權限,惡意應用程式可能會從下載供應商擷取所有項目,其中可能包括檔案名稱、說明、標題、路徑、網址等潛在敏感資訊,以及所有已下載檔案的完整讀取/寫入權限。惡意應用程式可能會在背景執行,監控所有下載內容並遠端洩漏內容,或在合法要求者存取檔案前,即時修改檔案。這可能會導致使用者無法使用核心應用程式,包括無法下載更新。
- 下載內容供應器 SQL 注入攻擊:惡意應用程式可透過 SQL 注入攻擊安全漏洞,從下載內容供應器擷取所有項目,且不需要任何權限。此外,具有有限權限的應用程式 (例如
android.permission.INTERNET) 也可以從其他 URI 存取所有資料庫內容。系統可能會擷取潛在的私密資訊,例如檔案名稱、說明、標題、路徑、網址,而且視權限而定,也可能存取已下載的內容。 - 下載供應商要求標頭資訊揭露 - 惡意應用程式只要取得
android.permission.INTERNET權限,就能從「下載供應商要求標頭」表格中擷取所有項目。這些標頭可能包含機密資訊,例如從 Android 瀏覽器或 Google Chrome (以及其他應用程式) 開始下載的任何工作階段 Cookie 或驗證標頭。攻擊者可能會在取得私密使用者資料的任何平台上,冒用該使用者身分。
(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 類別對不受信任的輸入內容幾乎不會進行驗證。
影響
如果利用外部儲存空間的 WRITE 權限,可能會導致安全漏洞。由於 android.permission.WRITE_EXTERNAL_STORAGE 權限可讓應用程式大範圍存取外部儲存空間,攻擊者可能會在使用者不知情的情況下修改檔案和下載內容、安裝潛在的惡意應用程式、拒絕為核心應用程式提供服務,或導致應用程式當機。惡意人士也可能操縱傳送至 Uri.parse() 的內容,導致使用者下載有害檔案。
因應措施
請直接在應用程式中設定下載作業,使用 HTTP 用戶端 (例如 Cronet)、程序排程器/管理工具,以及確保網路中斷時重試的方式,不要使用 DownloadManager。程式庫說明文件包含範例應用程式的連結,以及實作方式的說明。
如果應用程式需要管理程序排程、在背景執行下載作業,或在網路中斷後重試建立下載作業,請考慮加入 WorkManager 和 ForegroundServices。
以下是使用 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();
});
}
資源
- DownloadManager 的主要說明文件頁面
- DownloadManager CVE 報表
- Android 權限略過 CVE 2018-9468
- Android 下載供應器 SQL 注入 CVE-2018-9493
- Android 下載供應器權限旁路 CVE2018-9468
- Cronet 的主要說明文件頁面
- 如何在應用程式中使用 Cronet
- Cronet 導入範例
- Uri 的說明文件
- ForegroundService 說明文件
- WorkManager 說明文件