تطبيق إدارة التنزيل غير آمن

فئة OWASP: MASVS-NETWORK: Network Communication

نظرة عامة

‫DownloadManager هي خدمة نظام تم تقديمها في المستوى 9 لواجهة برمجة التطبيقات. ويعالج عمليات تنزيل HTTP التي تستغرق وقتًا طويلاً ويسمح للتطبيقات بتنزيل الملفات كأحد المهام التي تعمل في الخلفية. تتعامل واجهة برمجة التطبيقات مع تفاعلات HTTP وتعيد تنزيل الملفات بعد الأخطاء أو عند تغيير الاتصال وإعادة تشغيل النظام.

يتضمّن DownloadManager نقاط ضعف ذات صلة بالأمان تجعله خيارًا غير آمن لإدارة عمليات التنزيل في تطبيقات Android.

(1) نقاط ضعف CVE في مقدّم خدمة التنزيل

في عام 2018، تم العثور على ثلاثة CVE وإصلاحها في Download Provider. في ما يلي ملخّص لكلّ منها (اطّلِع على التفاصيل الفنية).

  • تجاوز إذن مقدّم خدمة التنزيل: بدون الأذونات الممنوحة، يمكن لتطبيق ضارٍ استرجاع جميع الإدخالات من مقدّم خدمة التنزيل، والتي يمكن أن تشمل معلومات حسّاسة محتملة، مثل أسماء الملفات والأوصاف والعناوين والمسارات وعناوين URL، بالإضافة إلى أذونات القراءة/الكتابة الكاملة لجميع الملفات التي تم تنزيلها. يمكن أن يعمل تطبيق ضار في الخلفية، ويتتبّع جميع عمليات التنزيل ويسرب محتوياتها عن بُعد، أو يعدّل الملفات أثناء التشغيل قبل أن يتم الوصول إليها من قِبل المُقدّم الشرعي للطلب. وقد يؤدي ذلك إلى حجب الخدمة للمستخدم في التطبيقات الأساسية، بما في ذلك عدم التمكّن من تنزيل التحديثات.
  • Download Provider SQL Injection – يمكن لتطبيق ضار ليس لديه أذونات استرداد جميع الإدخالات من موفر التنزيل، بسبب ثغرة إدخال SQL. بالإضافة إلى ذلك، يمكن للتطبيقات التي تمتلك أذونات محدودة، مثل android.permission.INTERNET، الوصول أيضًا إلى كل محتويات قاعدة البيانات من عنوان URI مختلف. يمكن استرداد معلومات يُحتمل أن تكون حسّاسة، مثل أسماء الملفات وأوصافها وعناوينها ومسارات الوصول إليها وعناوين URL، وقد يكون من الممكن أيضًا الوصول إلى المحتوى الذي تم تنزيله استنادًا إلى الأذونات.
  • تنزيل الإفصاح عن عناوين طلبات مقدِّم الخدمة: يمكن للتطبيق الضار الذي تم منحه إذن android.permission.INTERNET استرداد جميع الإدخالات من جدول عناوين طلبات موفّر التنزيل. وقد تتضمّن هذه الرؤوس معلومات حسّاسة، مثل ملفات تعريف الارتباط الخاصة بالجلسة أو رؤوس المصادقة لأي عملية تنزيل يتم بدءها من متصفّح Android أو Google Chrome، من بين تطبيقات أخرى. وقد يسمح ذلك للمهاجمين بدسِّ السم في طعام المستخدمين، أي انتحال هويتهم على أي منصة تم فيها الحصول على بياناتهم الحسّاسة.

(2) الأذونات الخطيرة

يتطلب DownloadManager في مستويات واجهة برمجة التطبيقات الأقل من 29 أذونات خطيرة – android.permission.WRITE_EXTERNAL_STORAGE. بالنسبة إلى المستوى 29 أو المستويات الأعلى لواجهة برمجة التطبيقات، android.permission.WRITE_EXTERNAL_STORAGE لا تكون الأذونات مطلوبة، ولكن يجب أن يشير عنوان URL إلى مسار ضمن الкатаلوگات التي يملكها التطبيق أو مسار ضمن الدليل "عمليات التنزيل" على مستوى أعلى.

(3) الاعتماد على Uri.parse()

تعتمد أداة DownloadManager على الطريقة Uri.parse() لتحليل موقع التنزيل المطلوب. من أجل تحسين الأداء، لا تطبّق فئة Uri سوى فحص بسيط أو لا تفحص على الإطلاق الإدخال غير الموثوق به.

التأثير

قد يؤدي استخدام DownloadManager إلى ثغرات أمنية من خلال استغلال أذونات الكتابة في وحدة التخزين الخارجية. بما أنّ أذونات android.permission.WRITE_EXTERNAL_STORAGE تسمح بالوصول الواسع النطاق إلى مساحة التخزين الخارجية، من الممكن للمهاجم أن يعدّل الملفات وعمليات التنزيل بدون تنبيه أو أن يثبِّت تطبيقات قد تكون ضارة أو يمنع الوصول إلى التطبيقات الأساسية أو يتسبّب في تعطُّل التطبيقات. يمكن للجهات الفاعلة الضارّة أيضًا التلاعب بما يتم إرساله إلى دالة Uri.parse()‎ لحثّ المستخدم على تنزيل ملف ضار.

إجراءات التخفيف

بدلاً من استخدام DownloadManager، يمكنك إعداد عمليات التنزيل مباشرةً في تطبيقك باستخدام عميل HTTP (مثل Cronet) وأداة جدولة/مدير العمليات وطريقة لضمان إعادة المحاولة في حال فقدان الشبكة. تتضمّن مستندات المكتبة رابطًا يؤدي إلى نموذج تطبيق بالإضافة إلى تعليمات حول كيفية تنفيذه.

إذا كان تطبيقك يتطلّب إمكانية إدارة جدولة العمليات أو تنفيذ عمليات تنزيل في الخلفية أو إعادة محاولة إجراء عملية التنزيل بعد فقدان شبكة الاتصال، ننصحك بتضمين WorkManager وForegroundServices.

في ما يلي مثال على الرمز البرمجي لإعداد عملية تنزيل باستخدام Cronet، وهو مأخوذ من الدرس التطبيقي حول 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();
    });
}

المراجع