← Back to writing
Mobile / Android

Android Data Storage Security

Oct 19, 2024
4 min read
lawbyte

Insecure data storage is one of the most consistent findings in Android app assessments. Sensitive data ends up in plaintext SharedPreferences, world-readable SQLite databases, unprotected external storage, and backup files. This post covers every storage mechanism and how to assess it.

App Data Directory Structure

Each app has a private directory at /data/data/<package_name>/:

/data/data/com.target.app/
├── shared_prefs/ ← SharedPreferences XML files
├── databases/ ← SQLite databases
├── files/ ← General files
├── cache/ ← Cache files (may be cleared by system)
├── app_webview/ ← WebView data (cookies, localStorage)
└── lib/ ← Native libraries

Access requires root, or adb shell run-as com.target.app if debuggable=true.

# List all files in app directory (root required)
adb shell "find /data/data/com.target.app -type f"

# With debuggable=true, no root needed
adb shell run-as com.target.app find /data/data/com.target.app -type f

SharedPreferences

SharedPreferences stores key-value pairs as XML. Apps commonly store session tokens, user data, and flags here.

# List all preference files
adb shell run-as com.target.app ls /data/data/com.target.app/shared_prefs/

# Read a preference file
adb shell run-as com.target.app cat /data/data/com.target.app/shared_prefs/user_prefs.xml

Typical findings:

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="auth_token">eyJhbGci...</string>
<string name="user_password">P@ssw0rd123</string>
<boolean name="is_premium" value="true" />
<string name="user_id">10043</string>
</map>

Tamper via Frida:

Java.perform(function() {
var ctx = Java.use('android.app.ActivityThread').currentApplication();
var sp = ctx.getSharedPreferences('user_prefs', 0);
var editor = sp.edit();
editor.putBoolean('is_premium', true);
editor.putString('role', 'admin');
editor.commit();
console.log('[*] SharedPreferences modified');
});

SQLite Databases

# Find all databases
adb shell run-as com.target.app find /data/data/com.target.app/databases/ -name "*.db"

# Pull the database
adb pull /data/data/com.target.app/databases/app.db .

# Open with sqlite3
sqlite3 app.db

# Inside sqlite3:
.tables # list all tables
.schema users # show table structure
SELECT * FROM users; # dump all users
SELECT * FROM sessions; # session tokens
SELECT * FROM messages; # private messages

# Export to CSV
.mode csv
.output dump.csv
SELECT * FROM users;
.output stdout

Common sensitive tables: users, sessions, tokens, messages, credentials, payments, health_data

SQL Injection in Content Providers

If the app exposes a ContentProvider:

# drozer
run app.provider.query content://com.target.app.provider/users \
--projection "* FROM users--"

run app.provider.query content://com.target.app.provider/users \
--selection "1=1) UNION SELECT name,sql,3,4,5 FROM sqlite_master--"

Internal Files

# Find all files, sorted by modification time
adb shell run-as com.target.app find /data/data/com.target.app/files/ -type f

# Look for logs, configs, keys
adb shell run-as com.target.app ls /data/data/com.target.app/files/
adb pull /data/data/com.target.app/files/config.json .
adb pull /data/data/com.target.app/files/private_key.pem .

External Storage

Files on external storage (/sdcard/) are readable by any app with READ_EXTERNAL_STORAGE permission (or no permission on older Android).

# Check what the app writes to external storage
adb shell ls /sdcard/Android/data/com.target.app/
adb shell ls /sdcard/Download/
adb shell find /sdcard/ -name "*.log" -o -name "*.db" -o -name "*.json"

Hook file writes to catch external storage usage:

Java.perform(function() {
var FileOutputStream = Java.use('java.io.FileOutputStream');
FileOutputStream.$init.overload('java.io.File', 'boolean').implementation = function(file, append) {
var path = file.getAbsolutePath();
if (path.includes('/sdcard/') || path.includes('/storage/')) {
console.log('[!] External write: ' + path);
}
return this.$init(file, append);
};
});

WebView Data

Apps using WebView store cookies and localStorage in the app’s directory:

# WebView cookies database
adb pull /data/data/com.target.app/app_webview/Default/Cookies .
sqlite3 Cookies
SELECT host_key, name, value FROM cookies;

# WebView localStorage
adb pull /data/data/com.target.app/app_webview/Default/Local\ Storage/ .

# Session storage, IndexedDB
ls /data/data/com.target.app/app_webview/Default/

Android Keystore — Misuse Patterns

The Android Keystore is the secure key storage. Even when used, it can be misused.

Keys not protected with user authentication

// Vulnerable — key can be used even if device is not unlocked
KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(
"myKey", KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_AES)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
// Missing: .setUserAuthenticationRequired(true)
.build();

Keys backed up in cloud (pre-Android 9)

On Android < 9, keystore keys could be included in cloud backups.

Extract key material via Frida

Java.perform(function() {
var SecretKeyFactory = Java.use('javax.crypto.SecretKeyFactory');
SecretKeyFactory.generateSecret.implementation = function(keySpec) {
var result = this.generateSecret(keySpec);
// If it's a PBEKeySpec, extract the password
try {
var PBEKeySpec = Java.use('javax.crypto.spec.PBEKeySpec');
var spec = Java.cast(keySpec, PBEKeySpec);
console.log('[*] Key password: ' + new java.lang.String(spec.getPassword()));
} catch(e) {}
return result;
};
});

Backup Extraction (allowBackup=true)

# Trigger backup
adb backup -noapk com.target.app -f backup.ab

# Wait for "Backup finished" on device, then extract
dd if=backup.ab bs=1 skip=24 | python3 -c \
"import zlib,sys; sys.stdout.buffer.write(zlib.decompress(sys.stdin.buffer.read()))" \
| tar -xvf -

# Browse extracted files
find . -type f | head -30

Clipboard Monitoring

Apps may copy sensitive data to the clipboard. Intercept it:

Java.perform(function() {
var ClipboardManager = Java.use('android.content.ClipboardManager');
ClipboardManager.setPrimaryClip.implementation = function(data) {
var text = data.getItemAt(0).getText();
console.log('[*] Clipboard set: ' + text);
return this.setPrimaryClip(data);
};
});

Log Leakage

# Capture all app logs (requires adb)
adb logcat -d | grep com.target.app

# Or filter by tag
adb logcat -s "MyApp" "okhttp" "Retrofit"

# Look for sensitive data in logs
adb logcat -d | grep -iE "token|password|pin|secret|key|auth"

Hook Log.d/e/i/v to capture all log output via Frida:

Java.perform(function() {
['d','e','i','v','w'].forEach(function(level) {
var Log = Java.use('android.util.Log');
Log[level].overload('java.lang.String', 'java.lang.String').implementation = function(tag, msg) {
console.log('[Log.' + level + '] ' + tag + ': ' + msg);
return this[level](tag, msg);
};
});
});

Storage Testing Checklist

  • SharedPreferences contains no sensitive data in plaintext
  • SQLite databases do not contain credentials or tokens
  • Files in internal storage are not world-readable
  • App does not write sensitive data to external storage
  • WebView cookies are HttpOnly and Secure
  • Keystore keys require user authentication
  • allowBackup=false in manifest (or sensitive data excluded)
  • No sensitive data in logcat
  • No sensitive data copied to clipboard without user action
  • Cache files do not contain sensitive data

Discussion

Leave a comment · All fields required · No spam

No comments yet. Be the first.