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

หมวดหมู่ OWASP: MASVS-NETWORK: การสื่อสารผ่านเครือข่าย

ภาพรวม

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

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

(1) CVE ในผู้ให้บริการดาวน์โหลด

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

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

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

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

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

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

ผลกระทบ

การใช้ DownloadManager อาจทำให้เกิดช่องโหว่ด้วยการใช้ประโยชน์จากสิทธิ์ WRITE ในที่จัดเก็บข้อมูลภายนอก เนื่องจากสิทธิ์ android.permission.WRITE_EXTERNAL_STORAGE อนุญาตให้เข้าถึงที่เก็บข้อมูลภายนอกได้แบบกว้าง ผู้โจมตีจึงอาจแก้ไขไฟล์และการดาวน์โหลดโดยที่ผู้ใช้ไม่รู้ตัว ติดตั้งแอปที่อาจเป็นอันตราย ปฏิเสธบริการแก่แอปหลัก หรือทำให้แอปขัดข้อง ผู้ไม่ประสงค์ดียังอาจดัดแปลงสิ่งที่ส่งไปยัง Uri.parse() เพื่อทําให้ผู้ใช้ดาวน์โหลดไฟล์ที่เป็นอันตรายได้

การลดปัญหา

แทนที่จะใช้ DownloadManager ให้ตั้งค่าการดาวน์โหลดในแอปโดยตรงโดยใช้ไคลเอ็นต์ HTTP (เช่น Cronet) ตัวจัดตารางเวลา/ตัวจัดการกระบวนการ และวิธีตรวจสอบการลองอีกครั้งหากเครือข่ายขาดการเชื่อมต่อ เอกสารประกอบของไลบรารีจะมีลิงก์ไปยังแอปตัวอย่าง รวมถึงวิธีการในการใช้งาน

หากแอปพลิเคชันจำเป็นต้องจัดการการกำหนดเวลากระบวนการ เรียกใช้การดาวน์โหลดในเบื้องหลัง หรือลองสร้างการดาวน์โหลดอีกครั้งหลังจากเครือข่ายขาดการเชื่อมต่อ ให้พิจารณารวม WorkManager และ ForegroundServices

โค้ดตัวอย่างสำหรับการตั้งค่าการดาวน์โหลดโดยใช้ Cronet มีดังนี้ ซึ่งนำมาจาก codelab ของ 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();
    });
}

แหล่งข้อมูล