외부 저장소에 저장된 민감한 정보

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을 타겟팅하는 애플리케이션의 경우 개발자는 범위 지정 저장소를 명시적으로 선택할 수 있습니다. 이는 requestLegacyExternalStorage 플래그를 false 로 설정하여 AndroidManifest.xml 파일에서 달성할 수 있습니다. 범위 지정 저장소를 사용하면 애플리케이션은 외부 저장소에서 직접 만든 파일 또는 오디오 및 동영상과 같은 MediaStore API를 사용하여 저장된 파일 유형에만 액세스할 수 있습니다. 이렇게 하면 사용자 개인 정보 보호 및 보안에 도움이 됩니다.

Android 11 이상

Android 11 이상 버전을 타겟팅하는 애플리케이션의 경우 OS는 범위 지정 저장소 사용을 적용합니다 . 즉, 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!");
        }
    }
}

리소스