Unprotected Storage of Credentials
ID |
java.android_unprotected_storage_of_credentials |
Severity |
low |
Resource |
Misconfiguration |
Language |
Java |
Tags |
CWE:256, CWE:312, CWE:522, MASVS:MSTG-STORAGE-1, MASVS:MSTG-STORAGE-2, OWASP:2021:A02, OWASP:2024:M9, PCI-DSS:6.5.3, android, credentials, plaintext-storage |
Description
Storing sensitive credentials (passwords, API keys, tokens, secrets) in plaintext exposes them to attackers through physical device access, malicious applications, device backups, system logs, and forensic analysis. Android provides secure storage mechanisms that encrypt data using hardware-backed keys.
Rationale
Unencrypted credential storage, such as plaintext storage of a password enables:
-
Account compromise through extracted passwords
-
API abuse via stolen access tokens
-
Session hijacking using captured session IDs
-
Cryptographic key theft allowing data decryption
Storing sensitive data without adequate protection is also checked. Sensitive data may persist in multiple locations:
-
SharedPreferences XML files (
/data/data/<package>/shared_prefs/) -
SQLite databases (
/data/data/<package>/databases/) -
Internal/external file storage
-
Device backups (cloud or local)
Even with MODE_PRIVATE, standard Android storage is vulnerable on:
-
Rooted devices (root access bypasses permission model)
-
Devices with unlocked bootloaders
-
Malicious apps exploiting vulnerabilities
-
Physical device access (forensic tools)
This detector identifies plaintext credential storage across multiple mechanisms. Some patterns of vulnerable code follow:
Unencrypted SharedPreferences
Standard SharedPreferences stores data in plaintext XML, at /data/data/{app package}/shared_prefs/user_prefs.xml:
// VULNERABLE: Plaintext SharedPreferences
val sharedPrefs = getSharedPreferences("user_prefs", Context.MODE_PRIVATE)
sharedPrefs.edit()
.putString("password", userPassword)// FLAW
.putString("api_token", apiToken)// FLAW
.apply()
which ends up in cleartext:
<map>
<string name="password">MySecretPass123</string>
<string name="api_token">sk_live_1234567890</string>
</map>
Insecure File Storage
Writing credentials to files without encryption:
// VULNERABLE: FileOutputStream with plaintext credentials
val fos = openFileOutput("credentials.txt", Context.MODE_PRIVATE)
fos.write("password=$password".toByteArray())// FLAW
fos.close()
// VULNERABLE: External storage (accessible by all apps)
val file = File(Environment.getExternalStorageDirectory(), "config.txt")
file.writeText("apiKey=$apiKey")// FLAW
Plaintext Database Storage
SQLite databases without encryption:
// VULNERABLE: Unencrypted SQLite
val db = openOrCreateDatabase("user.db", MODE_PRIVATE, null)
db.execSQL(// FLAW
"INSERT INTO users VALUES ('$username', '$password', '$apiToken')"
)
Database stored at: /data/data/com.example.app/databases/user.db (plaintext)
Remediation
-
Use EncryptedSharedPreferences
Encrypt SharedPreferences with AES256-GCM:
// SECURE: EncryptedSharedPreferences import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey val masterKey = MasterKey.Builder(context) .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .build() val encryptedPrefs = EncryptedSharedPreferences.create( context, "secret_prefs", masterKey, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ) // Keys and values are encrypted automatically encryptedPrefs.edit() .putString("password", userPassword) .putString("api_token", apiToken) .apply()Library:
androidx.security:security-crypto:1.1.0-alpha06 -
Use EncryptedFile for File Storage
// SECURE: EncryptedFile import androidx.security.crypto.EncryptedFile val encryptedFile = EncryptedFile.Builder( context, File(context.filesDir, "credentials.enc"), masterKey, EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB ).build() // Write encrypted data encryptedFile.openFileOutput().use { it.write(sensitiveData.toByteArray()) } -
Use SQLCipher for Database Encryption
// SECURE: SQLCipher import net.sqlcipher.database.SQLiteDatabase SQLiteDatabase.loadLibs(context) val password = "strong_db_password" val db = SQLiteDatabase.openOrCreateDatabase( databaseFile, password, null ) // All database content is encrypted db.execSQL("INSERT INTO users VALUES (?, ?, ?)", arrayOf(username, password, token))Library:
net.zetetic:android-database-sqlcipher:4.5.4 -
Use Android Keystore for Cryptographic Keys
// SECURE: Android Keystore (hardware-backed) val keyStore = KeyStore.getInstance("AndroidKeyStore") keyStore.load(null) val keyGenerator = KeyGenerator.getInstance( KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore" ) keyGenerator.init( KeyGenParameterSpec.Builder( "myKey", KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT ) .setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) .setUserAuthenticationRequired(true) // Requires biometric/PIN .build() ) val secretKey = keyGenerator.generateKey() -
Alternative: Hash Passwords (for local verification)
// SECURE: Store only password hashes import java.security.MessageDigest fun hashPassword(password: String, salt: ByteArray): String { val digest = MessageDigest.getInstance("SHA-256") val saltedPassword = password.toByteArray() + salt return digest.digest(saltedPassword).joinToString("") { "%02x".format(it) } } // Store hash instead of plaintext val salt = generateSecureRandomSalt() val passwordHash = hashPassword(userPassword, salt) sharedPrefs.edit() .putString("password_hash", passwordHash) .putString("salt", salt.toBase64()) .apply()
References
-
Android Developer: EncryptedSharedPreferences
-
Android Developer: EncryptedFile
-
Android Developer: Android Keystore System
-
OWASP MASTG-TEST-0001: Testing Local Storage for Sensitive Data
-
OWASP MASTG-TEST-0003: Testing Logs for Sensitive Data
-
SQLCipher: SQLCipher for Android
-
CWE-256: Plaintext Storage of a Password
-
CWE-312: Cleartext Storage of Sensitive Information
-
CWE-522: Insufficiently Protected Credentials