البيانات الحسّاسة المخزَّنة في مساحة التخزين الخارجية

فئة OWASP: MASVS-STORAGE: مساحة التخزين

نظرة عامة

لا تفرض التطبيقات التي تستهدف الإصدار 10 من نظام التشغيل Android (المستوى 29 لواجهة برمجة التطبيقات) أو الإصدارات الأقدم استخدام مساحة التخزين المحدودة. وهذا يعني أنّه يمكن لأي تطبيق آخر لديه إذن READ_EXTERNAL_STORAGE الوصول إلى أي بيانات مخزّنة في وحدة التخزين الخارجية.

التأثير

في التطبيقات التي تستهدف الإصدار 10 من نظام التشغيل Android (المستوى 29 لواجهة برمجة التطبيقات) أو الإصدارات الأقدم، إذا تم تخزين بيانات حساسة في مساحة التخزين الخارجية، يمكن لأي تطبيق على الجهاز لديه إذن READ_EXTERNAL_STORAGE الوصول إليها. ويسمح ذلك للتطبيقات الضارة بالوصول إلى الملفات الحساسة المخزَّنة بشكل دائم أو مؤقت في وحدة التخزين الخارجية بدون علم المستخدم. بالإضافة إلى ذلك، بما أنّه يمكن لأي تطبيق على النظام الوصول إلى المحتوى المخزّن على وحدة التخزين الخارجية، يمكن لأي تطبيق ضار يطلب أيضًا الإذن WRITE_EXTERNAL_STORAGE أن يتلاعب بالملفات المخزّنة على وحدة التخزين الخارجية، مثلاً لإضافة بيانات ضارة. وإذا تم تحميل هذه البيانات الضارة في التطبيق، قد تكون مصمَّمة لخداع المستخدمين أو حتى تنفيذ رموز برمجية.

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

مساحة التخزين المحصورة (الإصدار 10 من نظام التشغيل Android والإصدارات الأحدث)

Android 10

بالنسبة إلى التطبيقات التي تستهدف الإصدار 10 من نظام التشغيل Android، يمكن للمطوّرين الموافقة صراحةً على استخدام ميزة "مساحة التخزين المحصورة". ويمكنك إجراء ذلك من خلال ضبط العلامة requestLegacyExternalStorage على false في ملف AndroidManifest.xml. باستخدام ميزة "التخزين المحصور"، يمكن للتطبيقات الوصول فقط إلى الملفات التي أنشأتها بنفسها على مساحة التخزين الخارجية أو أنواع الملفات التي تم تخزينها باستخدام MediaStore API، مثل الملفات الصوتية والفيديوهات. ويساعد ذلك في حماية خصوصية المستخدمين وأمانهم.

الإصدار 11 من نظام التشغيل Android والإصدارات الأحدث

بالنسبة إلى التطبيقات التي تستهدف الإصدار 11 من نظام التشغيل Android أو الإصدارات الأحدث، يفرض نظام التشغيل استخدام مساحة التخزين المحصورة، أي أنّه يتجاهل العلامة requestLegacyExternalStorage ويحمي تلقائيًا مساحة التخزين الخارجية للتطبيقات من الوصول غير المرغوب فيه.

استخدام وحدة التخزين الداخلية للبيانات الحسّاسة

بغض النظر عن إصدار Android المستهدف، يجب دائمًا تخزين البيانات الحسّاسة للتطبيق في وحدة التخزين الداخلية. يتم تلقائيًا حصر إذن الوصول إلى وحدة التخزين الداخلية على التطبيق المالك بفضل ميزة الحماية في Android، وبالتالي يمكن اعتبارها آمنة، ما لم يتم تحديد جذر الجهاز.

تشفير البيانات الحسّاسة

إذا كانت حالات استخدام التطبيق تتطلّب تخزين بيانات حسّاسة على وحدة التخزين الخارجية، يجب تشفير البيانات. يُنصح باستخدام خوارزمية تشفير قوية، مع استخدام Android KeyStore لتخزين المفتاح بأمان.

بشكل عام، يُنصح بتشفير جميع البيانات الحساسة بغض النظر عن مكان تخزينها.

من المهم ملاحظة أنّ تشفير القرص بالكامل (أو التشفير المستند إلى الملفات من نظام التشغيل Android 10) هو إجراء يهدف إلى حماية البيانات من الوصول الفعلي إليها ومن غير ذلك من أساليب الهجوم. لهذا السبب، يجب أن يشفّر التطبيق البيانات الحسّاسة المخزّنة على وحدة تخزين خارجية بالإضافة إلى اتّخاذ إجراءات الأمان نفسها.

إجراء عمليات التحقّق من السلامة

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

Kotlin

package com.example.myapplication

import java.io.BufferedInputStream
import java.io.FileInputStream
import java.io.IOException
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException

object FileIntegrityChecker {
    @Throws(IOException::class, NoSuchAlgorithmException::class)
    fun getIntegrityHash(filePath: String?): String {
        val md = MessageDigest.getInstance("SHA-256") // You can choose other algorithms as needed
        val buffer = ByteArray(8192)
        var bytesRead: Int
        BufferedInputStream(FileInputStream(filePath)).use { fis ->
            while (fis.read(buffer).also { bytesRead = it } != -1) {
                md.update(buffer, 0, bytesRead)
            }

    }

    private fun bytesToHex(bytes: ByteArray): String {
        val sb = StringBuilder()
        for (b in bytes) {
            sb.append(String.format("%02x", b))
        }
        return sb.toString()
    }

    @Throws(IOException::class, NoSuchAlgorithmException::class)
    fun verifyIntegrity(filePath: String?, expectedHash: String): Boolean {
        val actualHash = getIntegrityHash(filePath)
        return actualHash == expectedHash
    }

    @Throws(Exception::class)
    @JvmStatic
    fun main(args: Array<String>) {
        val filePath = "/path/to/your/file"
        val expectedHash = "your_expected_hash_value"
        if (verifyIntegrity(filePath, expectedHash)) {
            println("File integrity is valid!")
        } else {
            println("File integrity is compromised!")
        }
    }
}

Java

package com.example.myapplication;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class FileIntegrityChecker {

    public static String getIntegrityHash(String filePath) throws IOException, NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance("SHA-256"); // You can choose other algorithms as needed
        byte[] buffer = new byte[8192];
        int bytesRead;

        try (BufferedInputStream fis = new BufferedInputStream(new FileInputStream(filePath))) {
            while ((bytesRead = fis.read(buffer)) != -1) {
                md.update(buffer, 0, bytesRead);
            }
        }

        byte[] digest = md.digest();
        return bytesToHex(digest);
    }

    private static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }

    public static boolean verifyIntegrity(String filePath, String expectedHash) throws IOException, NoSuchAlgorithmException {
        String actualHash = getIntegrityHash(filePath);
        return actualHash.equals(expectedHash);
    }

    public static void main(String[] args) throws Exception {
        String filePath = "/path/to/your/file";
        String expectedHash = "your_expected_hash_value";

        if (verifyIntegrity(filePath, expectedHash)) {
            System.out.println("File integrity is valid!");
        } else {
            System.out.println("File integrity is compromised!");
        }
    }
}

الموارد