Download Manager ที่ไม่ปลอดภัย

หมวดหมู่ OWASP: MASVS-NETWORK: Network Communication

ภาพรวม

DownloadManager เป็นบริการของระบบที่เปิดตัวในระดับ API 9 ซึ่งจัดการการดาวน์โหลด HTTP ที่ใช้เวลานานและอนุญาตให้แอปพลิเคชันดาวน์โหลดไฟล์เป็นงานเบื้องหลัง API ของบริการนี้จัดการการโต้ตอบ HTTP และลองดาวน์โหลดซ้ำหลังจากเกิดข้อผิดพลาดหรือเมื่อมีการเปลี่ยนแปลงการเชื่อมต่อและการรีบูตระบบ

DownloadManager มีจุดอ่อนที่เกี่ยวข้องกับความปลอดภัยซึ่งทำให้เป็นตัวเลือกที่ไม่ปลอดภัยสำหรับการจัดการการดาวน์โหลดในแอปพลิเคชัน Android

(1) CVE ใน Download Provider

ในปี 2018 พบและแก้ไข CVE 3 รายการใน Download Provider ข้อมูลสรุปของแต่ละรายการมีดังนี้ (ดู รายละเอียดทางเทคนิค)

  • การข้ามการให้สิทธิ์ Download Provider \- แอปที่เป็นอันตรายสามารถดึงข้อมูลทั้งหมดจาก Download Provider ได้โดยไม่มีการให้สิทธิ์ ซึ่งอาจรวมถึงข้อมูลที่อาจละเอียดอ่อน เช่น ชื่อไฟล์ คำอธิบาย ชื่อ เส้นทาง URL รวมถึงสิทธิ์ READ/WRITE แบบเต็มสำหรับไฟล์ที่ดาวน์โหลดทั้งหมด แอปที่เป็นอันตรายสามารถทำงานในเบื้องหลัง ตรวจสอบการดาวน์โหลดทั้งหมด และเปิดเผยเนื้อหาจากระยะไกล หรือแก้ไขไฟล์แบบเรียลไทม์ก่อนที่ผู้ขอที่ถูกต้องจะเข้าถึง ซึ่งอาจทำให้ผู้ใช้ไม่สามารถใช้งานแอปพลิเคชันหลักได้ รวมถึงไม่สามารถดาวน์โหลดการอัปเดต
  • การแทรก SQL ของ Download Provider \- แอปพลิเคชันที่เป็นอันตรายที่ไม่มีสิทธิ์สามารถดึงข้อมูลทั้งหมดจาก Download Provider ผ่านช่องโหว่การแทรก SQL นอกจากนี้ แอปพลิเคชันที่มีสิทธิ์จำกัด เช่น android.permission.INTERNET ยังสามารถเข้าถึงเนื้อหาฐานข้อมูลทั้งหมดจาก URI อื่นได้ด้วย ระบบอาจดึงข้อมูลที่อาจละเอียดอ่อน เช่น ชื่อไฟล์ คำอธิบาย ชื่อ เส้นทาง URL และอาจเข้าถึงเนื้อหาที่ดาวน์โหลดได้ด้วย ทั้งนี้ขึ้นอยู่กับสิทธิ์
  • การเปิดเผยข้อมูลส่วนหัวของคำขอ Download Provider \- แอปพลิเคชันที่เป็นอันตราย ที่ได้รับสิทธิ์ android.permission.INTERNET สามารถดึงข้อมูลทั้งหมดจากตารางส่วนหัวของคำขอ Download Provider ส่วนหัวเหล่านี้อาจมีข้อมูลที่ละเอียดอ่อน เช่น คุกกี้ของเซสชันหรือส่วนหัวการตรวจสอบสิทธิ์สำหรับการดาวน์โหลดที่เริ่มต้นจาก Android Browser หรือ Google Chrome รวมถึงแอปพลิเคชันอื่นๆ ซึ่งอาจอนุญาตให้ผู้โจมตีแอบอ้างเป็นผู้ใช้ในแพลตฟอร์มใดก็ตามที่ได้รับข้อมูลผู้ใช้ที่ละเอียดอ่อน

(2) สิทธิ์ที่เป็นอันตราย

DownloadManager ใน API ระดับต่ำกว่า 29 ต้องใช้สิทธิ์ที่เป็นอันตราย android.permission.WRITE_EXTERNAL_STORAGE. สำหรับ API ระดับ 29 ขึ้นไป android.permission.WRITE_EXTERNAL_STORAGE ไม่จำเป็นต้องมีสิทธิ์ แต่ URI ต้องอ้างอิงเส้นทางภายใน ไดเรกทอรีที่เป็นของแอปพลิเคชันหรือเส้นทางภายในไดเรกทอรี "Downloads" ระดับบนสุด

(3) การพึ่งพา Uri.parse()

DownloadManager อาศัยเมธอด Uri.parse() เพื่อแยกวิเคราะห์ตำแหน่งของการดาวน์โหลดที่ขอ คลาส Uri ใช้การตรวจสอบอินพุตที่ไม่น่าเชื่อถือเพียงเล็กน้อยหรือไม่ใช้เลยเพื่อประสิทธิภาพ

ผลกระทบ

การใช้ DownloadManager อาจนำไปสู่ช่องโหว่ผ่านการใช้สิทธิ์ WRITE กับพื้นที่เก็บข้อมูลภายนอก เนื่องจากสิทธิ์ 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();
    });
}

แหล่งข้อมูล