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 或更高版本为目标平台的应用,操作系统会强制使用分区存储,即忽略 requestLegacyExternalStorage 标志,并自动保护应用的外部存储空间免遭不必要的访问。
使用内部存储空间存储敏感数据
无论目标 Android 版本如何,应用的敏感数据都应始终存储在内部存储空间中。借助 Android 沙盒,对内部存储空间的访问权限会自动限制为仅限所有者应用,因此除非设备已获得 root 权限,否则可以认为内部存储空间是安全的。
加密敏感数据
如果应用的用例需要在外部存储空间中存储敏感数据,则应加密这些数据。建议使用强大的加密算法,并使用 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!");
}
}
}
资源
- 分区存储
- READ_EXTERNAL_STORAGE
- WRITE_EXTERNAL_STORAGE
- requestLegacyExternalStorage
- 数据和文件存储概览
- 数据存储(应用专用)
- 加密
- 密钥库
- 文件级加密
- 全盘加密