Sensitive Data Stored in External Storage

OWASP category: MASVS-STORAGE: Storage

Overview

Storing data in external storage without scoped storage has no security assurance. Writing sensitive files to external storage can result in exposure of sensitive data.

Impact

If sensitive data is stored in external storage, any application on the device can access it, exposing the data to unnecessary risk. Additionally, if the files stored in the external storage are then loaded into the application, it is possible that the data could have been tampered with in the meantime. The changed data could be designed to deceive users or even achieve code execution in the loading application.

Mitigations

Use Scoped Storage (Android 10 and later)

For Android version 10 and later, scoped storage can be used. This offers the capability to use external storage, while protecting the data from other applications. With scoped storage, applications can only access files that they have created themselves or files that are in the public Downloads folder. This helps protect user privacy and security.

Use Internal Storage

If possible, the internal storage of the application should be used, especially for sensitive data. Access to said storage is restricted to the owning application and therefore it can be considered as secure, unless the device is rooted.

Encrypt sensitive data

If sensitive data is stored in the external storage, it should be encrypted. A strong encryption algorithm is recommended, using the Android KeyStore to safely keep the key.

Remember that as a security-in-depth practice, all sensitive data should be encrypted, no matter where it is stored.

It is important to note that full disk encryption (or file-based encryption from Android 10) is a measure aimed to protect data from physical access and other attack vectors. For this specific instance, sensitive data should additionally be encrypted by the application.

Perform integrity checks

In cases where data or code has to be loaded from the external storage into the application, integrity checks to verify that no other application has tampered with this data or code are recommended. The hashes of the files should be stored in a secure manner, preferably encrypted and in the internal storage.

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!");
        }
    }
}

Resources