OWASP 类别: MASVS-NETWORK: 网络通信
概览
DownloadManager 是在 API level 9 中引入的一个系统服务。它处理长时间运行的 HTTP 下载,并允许应用作为后台任务下载文件。其 API 处理 HTTP 交互,并在失败、连接更改或系统重启后重试下载。
DownloadManager 存在安全相关漏洞,使其成为在 Android 应用中管理下载的不安全选择。
(1) Download Provider 中的 CVE
2018 年,在 Download Provider 中发现了三个 CVE 并已修补。每个 CVE 的摘要如下(参见技术详情)。
- Download Provider 权限绕过 – 在未获得任何权限的情况下,恶意应用可以从 Download Provider 检索所有条目,其中可能包含潜在的敏感信息,例如文件名、描述、标题、路径、URL,以及对所有已下载文件的完全读取/写入权限。恶意应用可以在后台运行,监视所有下载,远程泄露其内容,或者在合法请求者访问文件之前即时修改文件。这可能导致核心应用的用户遭遇拒绝服务,包括无法下载更新。
- Download Provider SQL 注入 – 通过 SQL 注入漏洞,未获得任何权限的恶意应用可以从 Download Provider 检索所有条目。此外,具有有限权限(例如
android.permission.INTERNET
)的应用也可以从不同的 URI 访问所有数据库内容。可以检索潜在的敏感信息,例如文件名、描述、标题、路径、URL,并且根据权限,也可能访问下载的内容。 - Download Provider 请求头信息泄露 – 获得
android.permission.INTERNET
权限的恶意应用可以从 Download Provider 请求头表中检索所有条目。这些请求头可能包含敏感信息,例如从 Android 浏览器或 Google Chrome 等应用启动的任何下载的会话 Cookie 或认证头。这可能允许攻击者在获取到用户敏感数据的任何平台上冒充用户。
(2) 危险权限
API level 低于 29 的 DownloadManager 需要危险权限 – android.permission.WRITE_EXTERNAL_STORAGE
。对于 API level 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)、流程调度器/管理器以及在网络中断时确保重试的方法来设置下载。该库的文档包含一个指向示例应用的链接以及关于如何实现的说明。
如果您的应用需要管理流程调度、在后台运行下载或在网络中断后重试建立下载的能力,那么考虑包含 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 Download Provider SQL 注入 CVE-2018- 9493
- Android Download Provider 权限绕过 CVE2018-9468
- Cronet 主文档页面
- 在应用中使用 Cronet 的说明
- Cronet 示例实现
- Uri 文档
- ForegroundService 文档
- WorkManager 文档