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

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 이상 버전을 타겟팅하는 애플리케이션의 경우 OS는 범위 지정 저장소 사용을 시행합니다. 즉, requestLegacyExternalStorage 플래그를 무시하고 애플리케이션의 외부 저장소를 원치 않는 액세스로부터 자동으로 보호합니다.

민감한 정보에 내부 저장소 사용

대상 Android 버전과 관계없이 애플리케이션의 민감한 정보는 항상 내부 저장소에 저장되어야 합니다. 내부 저장소에 대한 액세스는 Android 샌드박싱 덕분에 소유하고 있는 애플리케이션으로 자동 제한되므로 따라서 기기가 루팅되지 않았다면 안전한 것으로 간주될 수 있습니다.

민감한 정보 암호화

애플리케이션의 사용 사례에서 민감한 정보를 외부 데이터를 암호화해야 합니다 강력한 암호화 알고리즘은 Android 키 저장소를 사용하여 키를 안전하게 저장하는 것이 좋습니다.

일반적으로 민감한 정보가 저장된 위치와 관계없이 모든 민감한 정보를 암호화하는 것이 좋습니다.

전체 디스크 암호화(또는 Android 10의 파일 기반 암호화)는 물리적 액세스 및 기타 공격 벡터로부터 데이터를 보호하기 위한 조치입니다. 따라서 동일한 보안 조치를 허용하기 위해 외부 저장소에 보관된 데이터는 Google에서 추가적으로 암호화해야 애플리케이션입니다.

무결성 검사 실행

데이터나 코드를 외부 저장소에서 애플리케이션으로 로드해야 하는 경우 다른 애플리케이션에서 이 데이터나 코드를 조작하지 않았는지 확인하는 무결성 검사를 실행하는 것이 좋습니다. 파일의 해시는 안전한 방식으로 저장해야 하며, 가능하면 암호화하여 내부 저장소에 저장해야 합니다.

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

리소스