מנהל ההורדות לא בטוח

קטגוריה ב-OWASP: MASVS-NETWORK: Network Communication

סקירה כללית

DownloadManager הוא שירות מערכת שהוצג ברמת API 9. הוא מטפל בהורדות HTTP ממושכות ומאפשר לאפליקציות להוריד קבצים כמשימה ברקע. ה-API מטפל באינטראקציות של HTTP ומנסה שוב להוריד קבצים אחרי כישלונות, או אחרי שינויים בקישוריות והפעלות מחדש של המערכת.

ל-DownloadManager יש נקודות חולשה רלוונטיות לאבטחה, ולכן הוא בחירה לא מאובטחת לניהול הורדות באפליקציות ל-Android.

(1) CVEs ב-Download Provider

בשנת 2018 התגלו שלוש נקודות חולשה מסוג CVE ב-Download Provider, ותוקנו. בהמשך מופיע סיכום של כל אחת מהן (ראו פרטים טכניים).

  • עקיפת ההרשאות של ספק ההורדות – בלי הרשאות, אפליקציה זדונית עשויה לאחזר את כל הרשומות מספק ההורדות, שעשויות לכלול מידע רגיש כמו שמות קבצים, תיאורים, כותרות, נתיבים, כתובות URL, וגם הרשאות קריאה/כתיבה מלאות לכל הקבצים שהורדתם. אפליקציה זדונית יכולה לפעול ברקע, לעקוב אחרי כל ההורדות ולהדליף את התוכן שלהן מרחוק, או לשנות את הקבצים בזמן אמת לפני שמגיש הבקשה הלגיטימי ניגש אליהם. כתוצאה מכך, יכול להיות שתתרחש התקפת מניעת שירות (DoS) על האפליקציות המרכזיות של המשתמש, כולל חוסר יכולת להוריד עדכונים.
  • הזרקת SQL של ספק ההורדות – דרך נקודת חולשה של הזרקת SQL, אפליקציה זדונית ללא הרשאות יכולה לאחזר את כל הרשומות מספק ההורדות. בנוסף, אפליקציות עם הרשאות מוגבלות, כמו android.permission.INTERNET, יכולות לגשת לכל תוכן מסד הנתונים גם מ-URI אחר. יכול להיות שיהיה אפשר לאחזר מידע רגיש פוטנציאלי כמו שמות קבצים, תיאורים, כותרות, נתיבים וכתובות URL, וגם גישה לתוכן שהורדתם, בהתאם להרשאות.
  • חשיפת מידע בכותרות הבקשות של ספק ההורדות – אפליקציה זדונית שקיבלה את ההרשאה android.permission.INTERNET יכולה לאחזר את כל הרשומות בטבלת כותרות הבקשות של ספק ההורדות. הכותרות האלה יכולות לכלול מידע רגיש, כמו קובצי cookie של סשן או כותרות אימות, לכל הורדה שהתחילה מדפדפן 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 עלול להוביל לנקודות חולשה כתוצאה מניצול של הרשאות כתיבה לאחסון חיצוני. מאחר שההרשאות 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();
    });
}

משאבים