儲存在外部儲存空間的機密資料

OWASP 類別:MASVS-STORAGE:儲存空間

總覽

如果應用程式指定 Android 10 (API 29) 以下版本為目標,則不會強制執行限定範圍儲存空間。也就是說,只要其他應用程式具備 READ_EXTERNAL_STORAGE 權限,就能存取外部儲存空間中儲存的任何資料。

影響

如果應用程式是以 Android 10 (API 29) 以下版本為目標,且機密資料儲存在外部儲存空間,裝置上任何具備 READ_EXTERNAL_STORAGE 權限的應用程式都能存取這些資料。惡意應用程式可藉此永久或暫時存取外部儲存空間中儲存的機密檔案,此外,由於系統上的任何應用程式都能存取外部儲存空間的內容,因此只要惡意應用程式也宣告 WRITE_EXTERNAL_STORAGE 權限,就能竄改儲存在外部儲存空間中的檔案,例如加入惡意資料。如果將這類惡意資料載入應用程式,可能會欺騙使用者,甚至執行程式碼。

因應措施

限定範圍儲存空間 (Android 10 以上版本)

Android 10

如果應用程式指定 Android 10 為目標版本,開發人員可以明確選擇使用限定範圍儲存空間。如要達成此目的,請在 AndroidManifest.xml 檔案中將 requestLegacyExternalStorage 旗標設為 false。採用限定範圍儲存空間後,應用程式只能存取自己在外部儲存空間建立的檔案,或是使用 MediaStore API 儲存的檔案類型,例如音訊和影片。這有助於保護使用者隱私和安全。

Android 11 以上版本

如果應用程式指定 Android 11 以上版本,作業系統會強制使用限定範圍儲存空間,也就是忽略 requestLegacyExternalStorage 標記,並自動保護應用程式的外部儲存空間,避免遭到未經授權的存取。

使用內部儲存空間儲存機密資料

無論目標 Android 版本為何,應用程式的機密資料一律應儲存在內部儲存空間。由於 Android 沙箱機制,只有擁有應用程式才能存取內部儲存空間,因此除非裝置已啟用 Root 權限,否則可視為安全。

加密機密資料

如果應用程式的用途需要在外部儲存空間儲存機密資料,請務必加密資料。建議使用高強度加密演算法,並透過 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!");
        }
    }
}

資源