مدیر دانلود ناامن

دسته بندی OWASP: MASVS-NETWORK: ارتباطات شبکه

نمای کلی

دانلودمنیجر یک سرویس سیستمی است که در سطح API 9 معرفی شد. این سرویس دانلودهای طولانی مدت HTTP را مدیریت می‌کند و به برنامه‌ها اجازه می‌دهد تا فایل‌ها را به عنوان یک کار پس‌زمینه دانلود کنند. API آن تعاملات HTTP را مدیریت می‌کند و دانلودها را پس از شکست یا تغییرات اتصال و راه‌اندازی مجدد سیستم، دوباره امتحان می‌کند.

دانلود منیجر (DownloadManager) دارای نقاط ضعف امنیتی است که آن را به انتخابی ناامن برای مدیریت دانلودها در برنامه‌های اندروید تبدیل می‌کند.

(1) CVEها در ارائه‌دهنده‌ی دانلود

در سال ۲۰۱۸، سه آسیب‌پذیری CVE در Download Provider یافت و وصله شد. خلاصه‌ای از هر یک در ادامه آمده است (به جزئیات فنی مراجعه کنید).

  • دور زدن مجوزهای ارائه‌دهنده دانلود - بدون مجوزهای اعطا شده، یک برنامه مخرب می‌تواند تمام ورودی‌ها را از ارائه‌دهنده دانلود بازیابی کند، که می‌تواند شامل اطلاعات بالقوه حساس مانند نام فایل‌ها، توضیحات، عناوین، مسیرها، URLها و همچنین مجوزهای کامل خواندن/نوشتن برای همه فایل‌های دانلود شده باشد. یک برنامه مخرب می‌تواند در پس‌زمینه اجرا شود، تمام دانلودها را رصد کند و محتوای آنها را از راه دور فاش کند، یا فایل‌ها را در حین اجرا قبل از دسترسی درخواست‌کننده قانونی تغییر دهد. این می‌تواند باعث ایجاد اختلال در سرویس‌دهی برای کاربر برای برنامه‌های اصلی، از جمله عدم توانایی در دانلود به‌روزرسانی‌ها، شود.
  • تزریق SQL به ارائه‌دهنده دانلود - از طریق یک آسیب‌پذیری تزریق SQL، یک برنامه مخرب بدون مجوز می‌تواند تمام ورودی‌ها را از ارائه‌دهنده دانلود بازیابی کند. همچنین، برنامه‌هایی با مجوزهای محدود، مانند android.permission.INTERNET ، می‌توانند به تمام محتوای پایگاه داده از یک URI متفاوت دسترسی پیدا کنند. اطلاعات حساس بالقوه مانند نام فایل‌ها، توضیحات، عناوین، مسیرها، URLها می‌توانند بازیابی شوند و بسته به مجوزها، دسترسی به محتوای دانلود شده نیز ممکن است امکان‌پذیر باشد.
  • افشای اطلاعات سربرگ‌های درخواست دانلود ارائه‌دهنده - یک برنامه مخرب با مجوز android.permission.INTERNET می‌تواند تمام ورودی‌ها را از جدول سربرگ‌های درخواست دانلود ارائه‌دهنده بازیابی کند. این سربرگ‌ها ممکن است شامل اطلاعات حساس، مانند کوکی‌های جلسه یا سربرگ‌های احراز هویت، برای هر دانلودی که از مرورگر اندروید یا گوگل کروم و سایر برنامه‌ها شروع شده است، باشند. این امر می‌تواند به مهاجم اجازه دهد تا در هر پلتفرمی که داده‌های حساس کاربر از آن به دست آمده است، خود را به جای کاربر جا بزند.

(2) مجوزهای خطرناک

دانلودمنیجر در سطوح API پایین‌تر از ۲۹ به مجوزهای خطرناکی نیاز دارد - android.permission.WRITE_EXTERNAL_STORAGE . برای سطح API ۲۹ و بالاتر، مجوزهای android.permission.WRITE_EXTERNAL_STORAGE لازم نیست، اما URI باید به مسیری در دایرکتوری‌های متعلق به برنامه یا مسیری در دایرکتوری سطح بالای "Downloads" اشاره کند.

(3) تکیه بر Uri.parse()

DownloadManager برای تجزیه و تحلیل محل دانلود درخواستی به متد Uri.parse() متکی است. برای بهبود عملکرد، کلاس Uri اعتبارسنجی کمی روی ورودی‌های غیرقابل اعتماد اعمال می‌کند یا اصلاً اعمال نمی‌کند.

تأثیر

استفاده از DownloadManager ممکن است از طریق سوءاستفاده از مجوزهای WRITE به حافظه خارجی منجر به آسیب‌پذیری شود. از آنجایی که مجوزهای android.permission.WRITE_EXTERNAL_STORAGE امکان دسترسی گسترده به حافظه خارجی را فراهم می‌کنند، مهاجم می‌تواند بی‌سروصدا فایل‌ها و دانلودها را تغییر دهد، برنامه‌های بالقوه مخرب نصب کند، سرویس‌دهی به برنامه‌های اصلی را مسدود کند یا باعث خرابی برنامه‌ها شود. مهاجمان همچنین می‌توانند آنچه را که به Uri.parse() ارسال می‌شود دستکاری کنند تا کاربر یک فایل مضر را دانلود کند.

کاهش‌ها

به جای استفاده از DownloadManager، دانلودها را مستقیماً در برنامه خود با استفاده از یک کلاینت HTTP (مانند Cronet)، یک زمانبند/مدیر فرآیند و روشی برای اطمینان از تلاش مجدد در صورت قطع شبکه، تنظیم کنید. مستندات کتابخانه شامل پیوندی به یک برنامه نمونه و همچنین دستورالعمل‌هایی در مورد نحوه پیاده‌سازی آن است.

اگر برنامه شما نیاز به مدیریت زمان‌بندی فرآیندها، اجرای دانلودها در پس‌زمینه یا تلاش مجدد برای برقراری دانلود پس از قطع شدن شبکه دارد، می‌توانید WorkManager و ForegroundServices را نیز در نظر بگیرید.

کد نمونه برای تنظیم دانلود با استفاده از Cronet به شرح زیر است که از آزمایشگاه کد Cronet گرفته شده است.

کاتلین

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

جاوا

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

منابع