안전하지 않은 다운로드 관리자

OWASP 카테고리: MASVS-NETWORK: 네트워크 커뮤니케이션

개요

DownloadManager는 API 수준 9에 도입된 시스템 서비스입니다. 오래 실행되는 HTTP 다운로드를 처리하고 애플리케이션이 백그라운드 작업으로 파일을 다운로드할 수 있도록 합니다. API는 HTTP 상호작용을 처리하고 다운로드가 실패되거나 연결이 변경되거나 시스템이 재부팅된 후에 다운로드를 다시 시도합니다.

DownloadManager에는 Android 애플리케이션에서 다운로드를 관리하는 데 안전하지 않은 선택이 되도록 하는 보안 관련 약점이 있습니다.

(1) 다운로드 제공자의 CVE

2018년 다운로드 제공자에서 세 개의 CVE가 발견되어 패치가 적용되었습니다. 각각의 요약은 다음과 같습니다 (기술 세부정보 참고).

  • 다운로드 제공자 권한 우회 - 권한이 부여되지 않은 악성 앱이 다운로드 제공자에서 모든 항목을 가져올 수 있습니다. 여기에는 파일 이름, 설명, 제목, 경로, URL과 같은 민감한 정보와 다운로드된 모든 파일에 대한 전체 읽기/쓰기 권한이 포함될 수 있습니다. 악성 앱은 백그라운드에서 실행되어 모든 다운로드를 모니터링하고 콘텐츠를 원격으로 유출하거나, 합법적인 요청자가 액세스하기 전에 파일을 즉시 수정할 수 있습니다. 이로 인해 업데이트를 다운로드할 수 없는 등 핵심 애플리케이션에 대한 서비스 거부가 발생할 수 있습니다.
  • 다운로드 제공자 SQL 삽입 – SQL 삽입 취약점을 통해 권한이 없는 악성 애플리케이션이 다운로드 제공자의 모든 항목을 가져올 수 있습니다. 또한 android.permission.INTERNET와 같이 권한이 제한된 애플리케이션도 다른 URI에서 모든 데이터베이스 콘텐츠에 액세스할 수 있습니다. 파일 이름, 설명, 제목, 경로, URL과 같은 민감한 정보가 검색될 수 있으며, 권한에 따라 다운로드된 콘텐츠에 액세스할 수도 있습니다.
  • 다운로드 제공자 요청 헤더 정보 공개 - android.permission.INTERNET 권한이 부여된 악성 애플리케이션이 다운로드 제공자 요청 헤더 테이블에서 모든 항목을 가져올 수 있습니다. 이러한 헤더에는 Android 브라우저 또는 Google Chrome을 비롯한 애플리케이션에서 시작된 다운로드의 세션 쿠키나 인증 헤더와 같은 민감한 정보가 포함될 수 있습니다. 이렇게 하면 공격자가 민감한 사용자 데이터를 획득한 플랫폼에서 사용자를 가장할 수 있습니다.

(2) 위험한 권한

API 수준이 29보다 낮은 DownloadManager에는 위험한 권한(android.permission.WRITE_EXTERNAL_STORAGE)이 필요합니다. API 수준 29 이상에서는 android.permission.WRITE_EXTERNAL_STORAGE 권한이 필요하지 않지만 URI는 애플리케이션이 소유한 디렉터리 내 경로 또는 최상위 '다운로드' 디렉터리 내 경로를 참조해야 합니다.

(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();
    });
}

리소스