Administrador de descargas no seguro

Categoría de OWASP: MASVS-NETWORK: Comunicación de red

Descripción general

DownloadManager es un servicio del sistema que se introdujo en el nivel de API 9. Controla las descargas HTTP de larga duración y permite que las aplicaciones descarguen archivos como una tarea en segundo plano. Su API controla las interacciones HTTP y reintenta las descargas después de fallas o durante cambios de conectividad y reinicios del sistema.

DownloadManager tiene debilidades relevantes en materia de seguridad que la convierten en una opción poco segura a la hora de administrar descargas en aplicaciones para Android.

(1) CVE en el proveedor de descargas

En 2018, se encontraron tres CVE y se corrigieron en el proveedor de descargas. A continuación, se incluye un resumen de cada una (consulta los detalles técnicos).

  • Omisión de permisos del proveedor de descargas: Sin permisos otorgados, una app maliciosa podría recuperar todas las entradas del proveedor de descargas, lo que podría incluir información potencialmente sensible, como nombres de archivos, descripciones, títulos, rutas de acceso y URLs, así como permisos completos de LECTURA/ESCRITURA para todos los archivos descargados. Una app maliciosa podría ejecutarse en segundo plano, supervisar todas las descargas y filtrar su contenido de forma remota, o bien modificar los archivos sobre la marcha antes de que el solicitante legítimo acceda a ellos. Esto podría provocar una denegación del servicio para el usuario en las aplicaciones principales, lo que incluye la imposibilidad de descargar actualizaciones.
  • Inyección de SQL en el proveedor de descargas: A través de una vulnerabilidad de inyección de SQL, una aplicación maliciosa sin permisos podría recuperar todas las entradas del proveedor de descargas. Además, las aplicaciones con permisos limitados, como android.permission.INTERNET, también podrían acceder a todo el contenido de la base de datos desde un URI diferente. Se podría recuperar información potencialmente sensible, como nombres de archivos, descripciones, títulos, rutas de acceso y URLs, y, según los permisos, también es posible que se pueda acceder al contenido descargado.
  • Divulgación de información de los encabezados de solicitud del proveedor de descargas: Una aplicación maliciosa con el permiso android.permission.INTERNET otorgado podría recuperar todas las entradas de la tabla de encabezados de solicitud del proveedor de descargas. Estos encabezados pueden incluir información sensible, como cookies de sesión o encabezados de autenticación, para cualquier descarga que se inicie desde el navegador de Android o Google Chrome, entre otras aplicaciones. Esto podría permitir que un atacante suplantara la identidad del usuario en cualquier plataforma desde la que se obtuvieron datos sensibles del usuario.

(2) Permisos peligrosos

DownloadManager en niveles de API inferiores a 29 requiere permisos peligrosos: android.permission.WRITE_EXTERNAL_STORAGE. En el caso del nivel de API 29 y versiones posteriores, no se requieren permisos de android.permission.WRITE_EXTERNAL_STORAGE, pero el URI debe referirse a una ruta dentro de los directorios que pertenecen a la aplicación o a una ruta dentro del directorio "Downloads" de nivel superior.

(3) Dependencia de Uri.parse()

DownloadManager se basa en el método Uri.parse() para analizar la ubicación de la descarga solicitada. Para mejorar el rendimiento, la clase Uri aplica poca o ninguna validación en la entrada no confiable.

Impacto

El uso de DownloadManager puede generar vulnerabilidades a través del uso de permisos de ESCRITURA en el almacenamiento externo. Dado que los permisos android.permission.WRITE_EXTERNAL_STORAGE permiten un acceso amplio al almacenamiento externo, un atacante puede modificar archivos y descargas de forma silenciosa, instalar apps potencialmente maliciosas, denegar el servicio a las apps principales o hacer que falle. Los agentes maliciosos también podrían manipular lo que se envía a Uri.parse() para que el usuario descargue un archivo dañino.

Mitigaciones

En lugar de usar DownloadManager, configura las descargas directamente en tu app con un cliente HTTP (como Cronet), un programador o administrador de procesos y una forma de garantizar que se vuelvan a intentar si se pierde la red. La documentación de la biblioteca incluye un vínculo a una app de muestra, así como instrucciones para implementarla.

Si tu aplicación requiere la capacidad de administrar la programación de procesos, ejecutar descargas en segundo plano o reintentar establecer la descarga después de una pérdida de red, considera incluir WorkManager y ForegroundServices.

El siguiente es un código de ejemplo para configurar una descarga con Cronet, tomado del codelab de 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();
    });
}

Recursos