Datos sensibles almacenados en el almacenamiento externo

Categoría de OWASP: MASVS-STORAGE: Almacenamiento

Descripción general

Las aplicaciones orientadas a Android 10 (nivel de API 29) o versiones anteriores no aplican permisos almacenamiento. Esto significa que cualquier dato almacenado en el almacenamiento externo puede al que puede acceder cualquier otra aplicación con READ_EXTERNAL_STORAGE permiso.

Impacto

En las aplicaciones orientadas a Android 10 (nivel de API 29) o versiones anteriores, si los datos sensibles se almacenan en el almacenamiento externo, cualquier aplicación del dispositivo con el permiso READ_EXTERNAL_STORAGE puede acceder a ellos. Esto permite que las aplicaciones maliciosas accedan de forma silenciosa a archivos sensibles almacenados de forma permanente o temporal en el almacenamiento externo. Además, como cualquier app del sistema puede acceder al contenido del almacenamiento externo, cualquier aplicación maliciosa que también declare el permiso WRITE_EXTERNAL_STORAGE puede manipular los archivos almacenados en el almacenamiento externo, p. ej., para incluir datos maliciosos. Este contenido malicioso datos, si se cargan en la aplicación, podrían diseñarse para engañar a los usuarios o, incluso, lograr la ejecución del código.

Mitigaciones

Almacenamiento específico (Android 10 y versiones posteriores)

Android 10

En el caso de las aplicaciones orientadas a Android 10, los desarrolladores pueden habilitar de forma explícita el almacenamiento específico. Para ello, configura la marca requestLegacyExternalStorage en false en el archivo AndroidManifest.xml. Con el almacenamiento específico, las aplicaciones solo pueden acceder a los archivos que crearon en el almacenamiento externo o a los tipos de archivos que se almacenaron con la API de MediaStore, como audio y video. Esto ayuda a proteger la privacidad y la seguridad del usuario.

Android 11 y versiones posteriores

En el caso de las aplicaciones orientadas a Android 11 o versiones posteriores, el SO aplica el uso del almacenamiento específico, es decir, ignora la marca requestLegacyExternalStorage y protege automáticamente el almacenamiento externo de las aplicaciones del acceso no deseado.

Usa el almacenamiento interno para datos sensibles

Independientemente de la versión de Android a la que se segmenta, los datos sensibles de una aplicación siempre deben almacenarse en el almacenamiento interno. El acceso al almacenamiento interno está restringido automáticamente a la aplicación propietaria gracias a la zona de pruebas de Android, por lo que puede considerarse seguro, a menos que el dispositivo tenga permisos de administrador.

Encripta datos sensibles

Si los casos de uso de la aplicación requieren el almacenamiento de datos sensibles en el almacenamiento externo, los datos deben estar encriptados. Se recomienda un algoritmo de encriptación seguro con Android KeyStore para almacenar la clave de forma segura.

En general, encriptar todos los datos sensibles es una práctica de seguridad recomendada, no sin importar dónde estén almacenados.

Es importante destacar que la encriptación de disco completo (o la encriptación basada en archivos de Android 10) es una medida destinada a proteger los datos del acceso físico y otras vectores de ataque. Por este motivo, para otorgar la misma medida de seguridad, la aplicación también debe encriptar los datos sensibles almacenados en el almacenamiento externo.

Realiza verificaciones de integridad

En los casos en que se deban cargar datos o código desde el almacenamiento externo a la aplicación, se recomiendan las verificaciones de integridad para verificar que ninguna otra aplicación haya manipulado estos datos o código. Los valores hash de los archivos deben almacenarse de forma segura, preferiblemente encriptados y en el almacenamiento interno.

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

Recursos