OWASP 类别: MASVS-STORAGE: Storage
概览
定位到 Android 10 (API 29) 或更低版本的应用不会强制执行分区存储。这意味着外部存储上存储的任何数据都可以被任何其他具有 READ_EXTERNAL_STORAGE
权限的应用访问。
影响
在定位到 Android 10 (API 29) 或更低版本的应用中,如果敏感数据存储在外部存储上,则设备上任何具有 READ_EXTERNAL_STORAGE 权限的应用都可以访问它。这使得恶意应用能够静默访问永久或临时存储在外部存储上的敏感文件。此外,由于外部存储上的内容可以被系统上的任何应用访问,任何声明了 WRITE_EXTERNAL_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
- 数据和文件存储概览
- 数据存储(应用专属)
- 加密
- 密钥库
- 文件级加密
- 全盘加密