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

فئة OWASP: MASVS-NETWORK: Network Communication

نظرة عامة

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

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

(1) الثغرات الأمنية الشائعة والتعرّض لها في "موفّر التنزيل"

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

  • تجاوز إذن "مزوّد التنزيل": بدون منح أي أذونات، يمكن لتطبيق ضار استرداد جميع الإدخالات من "مزوّد التنزيل"، والتي يمكن أن تتضمّن معلومات حساسة محتملة، مثل أسماء الملفات وأوصافها وعناوينها ومساراتها وعناوين URL، بالإضافة إلى أذونات القراءة والكتابة الكاملة لجميع الملفات التي تم تنزيلها. يمكن أن يعمل تطبيق ضار في الخلفية، ويراقب جميع عمليات التنزيل ويسرّب محتواها عن بُعد، أو يعدّل الملفات أثناء تنزيلها قبل أن يصل إليها المستخدم الشرعي. وقد يؤدي ذلك إلى حرمان المستخدم من الخدمة في التطبيقات الأساسية، بما في ذلك عدم القدرة على تنزيل التحديثات.
  • ثغرة أمنية في SQL Injection في "مزوّد التنزيل": من خلال ثغرة أمنية في SQL Injection، يمكن لتطبيق ضار ليس لديه أذونات استرداد جميع الإدخالات من "مزوّد التنزيل". بالإضافة إلى ذلك، يمكن للتطبيقات التي لديها أذونات محدودة، مثل 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، ولكن يجب أن يشير معرّف الموارد المنتظم (URI) إلى مسار ضمن الدلائل التي يملكها التطبيق أو مسار ضمن دليل "عمليات التنزيل" ذي المستوى الأعلى.

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

الموارد