Sensitive Data Stored in External Storage

OWASP category: MASVS-STORAGE: Storage

Overview

Applications targeting Android 10 (API 29) or lower don't enforce scoped storage. This means that any data stored on the external storage can be accessed by any other application with the READ_EXTERNAL_STORAGE permission.

Impact

In applications targeting Android 10 (API 29) or lower, if sensitive data is stored on the external storage, any application on the device with the READ_EXTERNAL_STORAGE permission can access it. This allows malicious applications to silently access sensitive files permanently or temporarily stored on the external storage. Additionally, since content on the external storage can be accessed by any app on the system, any malicious application that also declares the WRITE_EXTERNAL_STORAGE permission can tamper with files stored on the external storage, e.g. to include malicious data. This malicious data, if loaded into the application, could be designed to deceive users or even achieve code execution.

Mitigations

Scoped Storage (Android 10 and later)

Android 10

For applications targeting Android 10, developers can explicitly opt-in to scoped storage. This can be achieved by setting the requestLegacyExternalStorage flag to false in the AndroidManifest.xml file. With scoped storage, applications can only access files that they have created themselves on the external storage or files types that were stored using the MediaStore API such as Audio and Video. This helps protect user privacy and security.

Android 11 and later

For applications targeting Android 11 or later versions, the OS enforces the use of scoped storage, i.e. it ignores the requestLegacyExternalStorage flag and automatically protects applications' external storage from unwanted access.

Use Internal Storage for Sensitive Data

Regardless of the targeted Android version, an application's sensitive data should always be stored on internal storage. Access to internal storage is automatically restricted to the owning application thanks to Android sandboxing, therefore it can be considered secure, unless the device is rooted.

Encrypt sensitive data

If the application's use cases require storing sensitive data on the external storage, the data should be encrypted. A strong encryption algorithm is recommended, using the Android KeyStore to safely store the key.

In general, encrypting all sensitive data is a recommended security practice, no matter where it is stored.

It is important to note that full disk encryption (or file-based encryption from Android 10) is a measure aimed at protecting data from physical access and other attack vectors. Because of this, to grant the same security measure, sensitive data held on external storage should additionally be encrypted by the application.

Perform integrity checks

In cases where data or code has to be loaded from the external storage into the application, integrity checks to verify that no other application has tampered with this data or code are recommended. The hashes of the files should be stored in a secure manner, preferably encrypted and in the internal storage.

Kotlin

packagecom.example.myapplication
importjava.io.BufferedInputStream
importjava.io.FileInputStream
importjava.io.IOException
importjava.security.MessageDigest
importjava.security.NoSuchAlgorithmException
objectFileIntegrityChecker{
@Throws(IOException::class,NoSuchAlgorithmException::class)
fungetIntegrityHash(filePath:String?):String{
valmd=MessageDigest.getInstance("SHA-256")// You can choose other algorithms as needed
valbuffer=ByteArray(8192)
varbytesRead:Int
BufferedInputStream(FileInputStream(filePath)).use{fis->
while(fis.read(buffer).also{bytesRead=it}!=-1){
md.update(buffer,0,bytesRead)
}
}
privatefunbytesToHex(bytes:ByteArray):String{
valsb=StringBuilder()
for(binbytes){
sb.append(String.format("%02x",b))
}
returnsb.toString()
}
@Throws(IOException::class,NoSuchAlgorithmException::class)
funverifyIntegrity(filePath:String?,expectedHash:String):Boolean{
valactualHash=getIntegrityHash(filePath)
returnactualHash==expectedHash
}
@Throws(Exception::class)
@JvmStatic
funmain(args:Array<String>){
valfilePath="/path/to/your/file"
valexpectedHash="your_expected_hash_value"
if(verifyIntegrity(filePath,expectedHash)){
println("File integrity is valid!")
}else{
println("File integrity is compromised!")
}
}
}

Java

packagecom.example.myapplication;
importjava.io.BufferedInputStream;
importjava.io.FileInputStream;
importjava.io.IOException;
importjava.security.MessageDigest;
importjava.security.NoSuchAlgorithmException;
publicclass FileIntegrityChecker{
publicstaticStringgetIntegrityHash(StringfilePath)throwsIOException,NoSuchAlgorithmException{
MessageDigestmd=MessageDigest.getInstance("SHA-256");// You can choose other algorithms as needed
byte[]buffer=newbyte[8192];
intbytesRead;
try(BufferedInputStreamfis=newBufferedInputStream(newFileInputStream(filePath))){
while((bytesRead=fis.read(buffer))!=-1){
md.update(buffer,0,bytesRead);
}
}
byte[]digest=md.digest();
returnbytesToHex(digest);
}
privatestaticStringbytesToHex(byte[]bytes){
StringBuildersb=newStringBuilder();
for(byteb:bytes){
sb.append(String.format("%02x",b));
}
returnsb.toString();
}
publicstaticbooleanverifyIntegrity(StringfilePath,StringexpectedHash)throwsIOException,NoSuchAlgorithmException{
StringactualHash=getIntegrityHash(filePath);
returnactualHash.equals(expectedHash);
}
publicstaticvoidmain(String[]args)throwsException{
StringfilePath="/path/to/your/file";
StringexpectedHash="your_expected_hash_value";
if(verifyIntegrity(filePath,expectedHash)){
System.out.println("File integrity is valid!");
}else{
System.out.println("File integrity is compromised!");
}
}
}

Resources

Content and code samples on this page are subject to the licenses described in the Content License. Java and OpenJDK are trademarks or registered trademarks of Oracle and/or its affiliates.

Last updated 2024年10月11日 UTC.