不安全的下载管理器

OWASP 类别: MASVS-NETWORK: 网络通信

概述

DownloadManager 是在 API 级别 9 中引入的一种系统服务。它处理长时间运行的 HTTP 下载,并允许应用程序将文件作为后台任务下载。其 API 处理 HTTP 交互并在失败或跨连接性更改和系统重启后重试下载。

DownloadManager 存在与安全相关的弱点,使其成为在 Android 应用程序中管理下载的不安全选择。

(1) 下载提供程序中的 CVE

在 2018 年,在下载提供程序中发现了三个 CVE 并进行了修复。以下是对每个 CVE 的总结(请参阅 技术细节)。

  • 下载提供程序权限绕过 – 在没有授予任何权限的情况下,恶意应用程序可以检索下载提供程序中的所有条目,其中可能包含潜在的敏感信息,例如文件名、描述、标题、路径、URL 以及对所有已下载文件的完全读/写权限。恶意应用程序可以在后台运行,监视所有下载并远程泄露其内容,或在合法请求者访问这些文件之前对其进行动态修改。这会导致用户核心应用程序出现拒绝服务,包括无法下载更新。
  • 下载提供程序 SQL 注入 – 通过 SQL 注入漏洞,没有权限的恶意应用程序可以检索下载提供程序中的所有条目。此外,具有有限权限的应用程序(例如 android.permission.INTERNET)也可以从不同的 URI 访问所有数据库内容。可以检索潜在的敏感信息,例如文件名、描述、标题、路径、URL,并且可能可以访问已下载的内容(具体取决于权限)。
  • 下载提供程序请求标头信息泄露 – 授予 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 可能通过利用对外部存储的写入权限来导致漏洞。由于 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();
    });
}

资源