Unsafe Download Manager

OWASP category: MASVS-NETWORK: Network Communication

Overview

DownloadManager is a system service introduced in API level 9. It handles long-running HTTP downloads and allows applications to download files as a background task. Its API handles HTTP interactions and retries downloads after failures or across connectivity changes and system reboots.

DownloadManager has security relevant weaknesses that make it an insecure choice for managing downloads in Android applications.

(1) CVEs in Download Provider

In 2018, three CVEs were found and patched in Download Provider. A summary of each follows (see technical details).

  • Download Provider Permission Bypass – With no granted permissions, a malicious app could retrieve all entries from the Download Provider, which could include potentially sensitive information such as file names, descriptions, titles, paths, URLs, as well as full READ/WRITE permissions to all downloaded files. A malicious app could run in the background, monitoring all downloads and leaking their contents remotely, or modifying the files on-the-fly before they are accessed by the legitimate requester. This could cause a denial-of-service for the user for core applications, including the inability to download updates.
  • Download Provider SQL Injection – Through a SQL injection vulnerability, a malicious application with no permissions could retrieve all entries from the Download Provider. Also, applications with limited permissions, such as android.permission.INTERNET, could also access all database contents from a different URI. Potentially sensitive information such as file names, descriptions, titles, paths, URLs could be retrieved, and, depending on permissions, access to downloaded contents may be possible as well.
  • Download Provider Request Headers Information Disclosure – A malicious application with the android.permission.INTERNET permission granted could retrieve all entries from the Download Provider request headers table. These headers may include sensitive information, such as session cookies or authentication headers, for any download started from the Android Browser or Google Chrome, among other applications. This could allow an attacker to impersonate the user on any platform from which sensitive user data was obtained.

(2) Dangerous Permissions

DownloadManager in API levels lower than 29 requires dangerous permissions – android.permission.WRITE_EXTERNAL_STORAGE. For API level 29 and higher, android.permission.WRITE_EXTERNAL_STORAGE permissions are not required, but the URI must refer to a path within the directories owned by the application or a path within the top-level "Downloads" directory.

(3) Reliance on Uri.parse()

DownloadManager relies on the Uri.parse() method to parse the location of the requested download. In the interest of performance, the Uri class applies little to no validation on untrusted input.

Impact

Using DownloadManager may lead to vulnerabilities through the exploitation of WRITE permissions to external storage. Since android.permission.WRITE_EXTERNAL_STORAGE permissions allow broad access to external storage, it is possible for an attacker to silently modify files and downloads, install potentially malicious apps, deny service to core apps, or cause apps to crash. Malicious actors could also manipulate what is sent to Uri.parse() to cause the user to download a harmful file.

Mitigations

Instead of using DownloadManager, set up downloads directly in your app using an HTTP client (such as Cronet), a process scheduler/manager, and a way to ensure retries if there is network loss. The documentation of the library includes a link to a sample app as well as instructions on how to implement it.

If your application requires the ability to manage process scheduling, run downloads in the background, or retry establishing the download after network loss, then consider including WorkManager and ForegroundServices.

Example code for setting up a download using Cronet is as follows, taken from the 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();
    });
}

Resources