外部ストレージに保存された機密データ

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

リソース