OWASP 类别:MASVS-NETWORK:网络通信
概览
DownloadManager 是 API 级别 9 中引入的一项系统服务。它可处理长时间运行的 HTTP 下载,并允许应用将下载文件作为后台任务运行。其 API 可处理 HTTP 互动,并在下载失败或连接发生更改以及系统重新启动后重新尝试下载。
DownloadManager 存在与安全性相关的缺陷,因此在 Android 应用中管理下载时,它并不是一个安全的选择。
(1) 下载提供程序中的 CVE
2018 年,在下载提供程序中发现了三个 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 必须引用应用所拥有的目录中的路径或顶级“下载”目录中的路径。
(3) 依赖 Uri.parse()
DownloadManager 依赖于 Uri.parse() 方法来解析所请求下载的位置。为了提高性能,Uri 类几乎不会对不受信任的输入进行验证。
影响
通过利用对外部存储空间的 WRITE 权限,使用 DownloadManager 可能会导致出现漏洞。由于 android.permission.WRITE_EXTERNAL_STORAGE 权限允许广泛访问外部存储空间,因此攻击者可能会悄悄修改文件和下载内容、安装可能有害的应用、拒绝向核心应用提供服务,或导致应用崩溃。恶意行为者还可以操纵发送到 Uri.parse() 的内容,导致用户下载有害文件。
缓解措施
您可以使用 HTTP 客户端(例如 Cronet)、进程调度程序/管理器,以及一种在网络丢失时确保重试的方式,直接在应用中设置下载,而不是使用 DownloadManager。该库的文档包含指向示例应用的链接,以及有关如何实现该库的说明。
如果您的应用需要能够管理进程调度、在后台运行下载或在网络丢失后重试建立下载,请考虑添加 WorkManager 和 ForegroundServices。
以下是使用 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();
});
}
资源
- DownloadManager 的主要文档页面
- DownloadManager CVE 的报告
- Android 权限绕过 CVE 2018-9468
- Android 下载提供程序 SQL 注入漏洞 CVE-2018-9493
- Android 下载提供程序权限绕过 CVE2018-9468
- Cronet 的主要文档页面
- 有关在应用中使用 Cronet 的说明
- Cronet 实现示例
- URI 文档
- ForegroundService 的文档
- WorkManager 文档