diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0070/MASTG-DEMO-0070.md b/demos/android/MASVS-STORAGE/MASTG-DEMO-0070/MASTG-DEMO-0070.md index f44a030a3fc..bdda1d9e5f0 100644 --- a/demos/android/MASVS-STORAGE/MASTG-DEMO-0070/MASTG-DEMO-0070.md +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0070/MASTG-DEMO-0070.md @@ -3,7 +3,50 @@ platform: android title: Sensitive Data Stored Unencrypted via Room Database id: MASTG-DEMO-0070 code: [kotlin] -test: MASTG-TEST-0306 -status: placeholder -note: This placeholder reserves the ID for a demo illustrating the insecure storage of sensitive data using the Android Room Persistence Library without integrating an external encryption solution (like SQLCipher). It directly corresponds to the MASTG-TEST-0306. +test: MASTG-TEST-0x01 +status: new --- + +## Sample + +The snippet below shows sample code that uses the [Android Room Persistence Library](https://developer.android.com/jetpack/androidx/releases/room) +to store sensitive data, including PII (email) and secrets (access token), +in **plaintext** without any encryption. + +{{ MastgTest.kt # MastgTest_reversed.java }} + +## Steps + +1. Make sure you have @MASTG-TOOL-0004 installed on your machine +2. Click the **Start** button +3. Execute `run.sh`. + +The script pulls the Room database called (`PrivateUnencryptedRoomDB`) from the application's sandbox along +with its `WAL`/`SHM` files and queries the `users` table content: + +{{ run.sh }} + +## Observation + +The output contains the extracted content from the `users` table, +showing the sensitive PII (email address) and the access token stored in **plaintext**. + +{{ output.txt }} + +## Evaluation + +The test case fails because the application persists sensitive information +in a Room database without any form of encryption, +making it accessible in plaintext to anyone with access to the app's private storage. + +Reviewing the evidence in `output.txt`: + +- Each row represents a record from the `users` table, formatted as `id|username|email|token`. +- The third field contains the user's **email address** (e.g., `john.doe@maswe.com`), +which is considered PII. +- The fourth field contains the **access token** (e.g., `ghp_123456...`), +which is a secret. + +Since these values are clearly readable via a standard SQLite query +and no encryption layer (like SQLCipher) is present in the `AppDatabase` configuration, +the security requirements for sensitive data storage are not met. diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0070/MastgTest.kt b/demos/android/MASVS-STORAGE/MASTG-DEMO-0070/MastgTest.kt new file mode 100644 index 00000000000..65d0921a5e0 --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0070/MastgTest.kt @@ -0,0 +1,103 @@ +// SUMMARY: This sample demonstrates how the Room Persistence Library stores sensitive data in plaintext by default when no encryption (like SQLCipher) is configured. +package org.owasp.mastestapp + +import android.content.Context +import android.util.Log +import androidx.room.* +import java.io.File + +@Entity(tableName = "users") +data class UserEntity( + @PrimaryKey(autoGenerate = true) val id: Int = 0, + val username: String, + // FAIL: [MASTG-TEST-0x01] Sensitive data, like PII (email) and secrets (access token) stored in plaintext within the Room database of the app. + val email: String, + val token: String +) + +@Dao +interface UserDao { + @Insert + fun insert(user: UserEntity) + + @Query("DELETE FROM users") + fun clear() + + @Query("SELECT * FROM users") + fun getAll(): List +} + +@Database(entities = [UserEntity::class], version = 1) +abstract class AppDatabase : RoomDatabase() { + abstract fun userDao(): UserDao + + companion object { + fun getInstance(ctx: Context): AppDatabase { + val dbPath = ctx.getDatabasePath("PrivateUnencryptedRoomDB") + Log.i("MASTG-ROOM", "Database absolute path: ${dbPath.absolutePath}") + + dbPath.parentFile?.let { + if (!it.exists()) { + Log.i("MASTG-ROOM", "Creating database directory: ${it.absolutePath}") + it.mkdirs() + } else { + Log.i("MASTG-ROOM", "Database directory exists: ${it.absolutePath}") + } + } + // Room database is built without a SupportSQLiteOpenHelper.Factory (like SQLCipher), + // which means the underlying SQLite file is not encrypted. + return Room.databaseBuilder( + ctx, + AppDatabase::class.java, + "PrivateUnencryptedRoomDB" + ) + .allowMainThreadQueries() + .build() + } + } +} + +class MastgTest(private val context: Context) { + + private fun writeSensitiveDataToUnencryptedRoom() { + val username = "demoUser_room" + val userEmail = "john.doe@maswe.com" + val accessToken = "ghp_1234567890abcdefghijklmnopqrstuvABCD" + + try { + val db = AppDatabase.getInstance(context) + val dao = db.userDao() + + Log.i("MASTG-ROOM", "Clearing existing users...") + dao.clear() + + Log.i("MASTG-ROOM", "Inserting new user...") + dao.insert( + UserEntity( + username = username, + email = userEmail, + token = accessToken + ) + ) + + val allUsers = dao.getAll() + Log.i("MASTG-ROOM", "Users in DB after insert: ${allUsers.size}") + allUsers.forEach { + Log.i("MASTG-ROOM", "User: ${it.username}, ${it.email}, ${it.token}") + } + + Log.i("MASTG-ROOM", "Room DB written successfully: PrivateUnencryptedRoomDB") + + } catch (e: Exception) { + Log.e("MASTG-ROOM", "Error writing Room DB: ${e.message}") + } + } + + fun mastgTest(): String { + Log.i("MASTG-ROOM", "Starting MastgTest RoomDB demo...") + writeSensitiveDataToUnencryptedRoom() + val dbPath = context.getDatabasePath("PrivateUnencryptedRoomDB") + Log.i("MASTG-ROOM", "Database final path: ${dbPath.absolutePath}") + return "RoomDB Demo Complete. Plaintext PII + Token stored in unencrypted Room database." + } +} diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0070/MastgTest_reversed.java b/demos/android/MASVS-STORAGE/MASTG-DEMO-0070/MastgTest_reversed.java new file mode 100644 index 00000000000..6c3d9519538 --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0070/MastgTest_reversed.java @@ -0,0 +1,62 @@ +package org.owasp.mastestapp; + +import android.content.Context; +import android.util.Log; +import java.io.File; +import java.util.Iterator; +import java.util.List; +import kotlin.Metadata; +import kotlin.jvm.internal.Intrinsics; + +/* compiled from: MastgTest.kt */ +@Metadata(d1 = {"\u0000\u001e\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0010\u0002\n\u0000\n\u0002\u0010\u000e\n\u0000\b\u0007\u0018\u00002\u00020\u0001B\u000f\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0004\b\u0004\u0010\u0005J\b\u0010\u0006\u001a\u00020\u0007H\u0002J\u0006\u0010\b\u001a\u00020\tR\u000e\u0010\u0002\u001a\u00020\u0003X\u0082\u0004¢\u0006\u0002\n\u0000¨\u0006\n"}, d2 = {"Lorg/owasp/mastestapp/MastgTest;", "", "context", "Landroid/content/Context;", "", "(Landroid/content/Context;)V", "writeSensitiveDataToUnencryptedRoom", "", "mastgTest", "", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48) +/* loaded from: classes3.dex */ +public final class MastgTest { + public static final int $stable = 8; + private final Context context; + + public MastgTest(Context context) { + Intrinsics.checkNotNullParameter(context, "context"); + this.context = context; + } + + private final void writeSensitiveDataToUnencryptedRoom() { + try { + try { + AppDatabase db = AppDatabase.INSTANCE.getInstance(this.context); + UserDao dao = db.userDao(); + Log.i("MASTG-ROOM", "Clearing existing users..."); + dao.clear(); + Log.i("MASTG-ROOM", "Inserting new user..."); + dao.insert(new UserEntity(0, "demoUser_room", "john.doe@maswe.com", "ghp_1234567890abcdefghijklmnopqrstuvABCD", 1, null)); + List allUsers = dao.getAll(); + Log.i("MASTG-ROOM", "Users in DB after insert: " + allUsers.size()); + List $this$forEach$iv = allUsers; + int $i$f$forEach = 0; + for (Iterator it = $this$forEach$iv.iterator(); it.hasNext(); it = it) { + Object element$iv = it.next(); + UserEntity it2 = (UserEntity) element$iv; + List allUsers2 = allUsers; + Log.i("MASTG-ROOM", "User: " + it2.getUsername() + ", " + it2.getEmail() + ", " + it2.getToken()); + allUsers = allUsers2; + $this$forEach$iv = $this$forEach$iv; + $i$f$forEach = $i$f$forEach; + } + Log.i("MASTG-ROOM", "Room DB written successfully: PrivateUnencryptedRoomDB"); + } catch (Exception e) { + e = e; + Log.e("MASTG-ROOM", "Error writing Room DB: " + e.getMessage()); + } + } catch (Exception e2) { + e = e2; + } + } + + public final String mastgTest() { + Log.i("MASTG-ROOM", "Starting MastgTest RoomDB demo..."); + writeSensitiveDataToUnencryptedRoom(); + File dbPath = this.context.getDatabasePath("PrivateUnencryptedRoomDB"); + Log.i("MASTG-ROOM", "Database final path: " + dbPath.getAbsolutePath()); + return "RoomDB Demo Complete. Plaintext PII + Token stored in unencrypted Room database."; + } +} diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0070/build.gradle.kts.libs b/demos/android/MASVS-STORAGE/MASTG-DEMO-0070/build.gradle.kts.libs new file mode 100644 index 00000000000..5737727158d --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0070/build.gradle.kts.libs @@ -0,0 +1,4 @@ +implementation("androidx.room:room-runtime:2.6.1") +implementation("androidx.room:room-ktx:2.6.1") +kapt("androidx.room:room-compiler:2.6.1") + diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0070/build.gradle.kts.plugins b/demos/android/MASVS-STORAGE/MASTG-DEMO-0070/build.gradle.kts.plugins new file mode 100644 index 00000000000..01c65ecb1bb --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0070/build.gradle.kts.plugins @@ -0,0 +1 @@ +kotlin("kapt") diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0070/output.txt b/demos/android/MASVS-STORAGE/MASTG-DEMO-0070/output.txt new file mode 100644 index 00000000000..dd6a5c718ce --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0070/output.txt @@ -0,0 +1,2 @@ +--- OBSERVATION: Unencrypted Room Database Content --- +1|demoUser_room|john.doe@maswe.com|ghp_1234567890abcdefghijklmnopqrstuvABCD diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0070/run.sh b/demos/android/MASVS-STORAGE/MASTG-DEMO-0070/run.sh new file mode 100755 index 00000000000..24269d492fd --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0070/run.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# Configuration +PACKAGE="org.owasp.mastestapp" +DB_NAME="PrivateUnencryptedRoomDB" +DB_PATH="databases" +OUTPUT_TXT_FILE="output.txt" + +echo "Extracting Room database files from sandbox..." + +for f in "$DB_NAME" "$DB_NAME-wal" "$DB_NAME-shm"; do + if adb exec-out run-as "$PACKAGE" cat "$DB_PATH/$f" > "$f" 2>/dev/null; then + echo "[+] Extracted $f ($(wc -c < "$f") bytes)" + else + echo "[-] Failed to extract $f" + fi +done + +echo "Inspecting database for plaintext sensitive data..." + +if [ -f "$DB_NAME" ]; then + echo "--- OBSERVATION: Unencrypted Room Database Content ---" > "$OUTPUT_TXT_FILE" + + echo "Querying 'users' table..." + sqlite3 "$DB_NAME" "SELECT * FROM users;" >> "$OUTPUT_TXT_FILE" + + echo "[+] Evidence collected in $OUTPUT_TXT_FILE:" + cat "$OUTPUT_TXT_FILE" +else + echo "ERROR: Database file could not be retrieved." + echo "Ensure the app is installed, debuggable, and has performed a write operation." + exit 1 +fi + +rm "$DB_NAME" "$DB_NAME-wal" "$DB_NAME-shm" 2>/dev/null + +echo "" +echo "[*] Evaluation: Test case fails if sensitive data (tokens/PII) is visible in plaintext above." diff --git a/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0306.md b/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0306.md deleted file mode 100644 index fe554fd1a44..00000000000 --- a/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0306.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: References to Sensitive Data Stored Unencrypted via Android Room DB -platform: android -id: MASTG-TEST-0306 -type: [static, code] -weakness: MASWE-0006 -best-practices: [] -profiles: [L1, L2] -status: placeholder -note: This test checks if the app uses the Android Room Persistence Library to store sensitive data (e.g., tokens, PII) without integrating an encryption layer (e.g., SQLCipher). It confirms the database file is stored in plaintext within the app's private sandbox. ---- diff --git a/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0x01.md b/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0x01.md new file mode 100644 index 00000000000..c4b30166133 --- /dev/null +++ b/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0x01.md @@ -0,0 +1,42 @@ +--- +platform: android +title: Runtime Storage of Unencrypted Data in Room Databases +id: MASTG-TEST-0x01 +type: [dynamic, filesystem] +weakness: MASWE-0006 +best-practices: [MASTG-BEST-0x25] +profiles: [L2] +status: new +--- + +## Overview + +This test complements @MASTG-TEST-0x02. It checks at runtime whether sensitive data (like tokens, secrets, or PII) is stored in Room databases without encryption. + +If the app stores sensitive data in the Room databases, such as tokens, credentials, or PII, without integrating an encryption layer like SQLCipher (@MASTG-KNOW-0038), anyone who gains access to the app's sandbox (via physical access or backup extraction) can extract the plaintext data. + +The goal is to ensure that sensitive information is not persisted in plaintext in the Room databases within the app's private storage. + +## Steps + +1. Exercise all the functionalities of the app that process or store sensitive data. + +2. Access the app's private storage (@MASTG-TECH-0008) and locate Room database files: + - `/data/data//databases/` + - `/data/data//databases/-wal` + - `/data/data//databases/-shm` + +3. Extract the database files of the app to the host machine (@MASTG-TECH-0002). + +4. Inspect database contents using a SQLite client or dynamic analysis tool to confirm whether sensitive data is stored in plaintext. + +## Observation + +The output should contain: + +- The location of the Room database files inside the application's private storage. +- Occurrences inside the Room database file where the sensitive data (tokens, secrets, PII) is stored in plaintext. + +## Evaluation + +The test fails if sensitive data in Room database files can be read in plaintext and no encryption mechanism (e.g., SQLCipher) is applied. diff --git a/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0x02.md b/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0x02.md new file mode 100644 index 00000000000..c98c5a25553 --- /dev/null +++ b/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0x02.md @@ -0,0 +1,42 @@ +--- +platform: android +title: References to Sensitive Data Unencrypted via Android Room Database +id: MASTG-TEST-0x02 +type: [static] +weakness: MASWE-0006 +best-practices: [MASTG-BEST-0x25] +profiles: [L2] +status: new +--- + +## Overview + +This test verifies whether the app's code uses the [Android Room Persistence Library](https://developer.android.com/training/data-storage/room) to store sensitive data — such as tokens, credentials, or PII — without encryption. By default, Room stores data in unencrypted SQLite databases. + +If the app stores sensitive data in the Room databases, such as tokens, credentials, or PII, without integrating an encryption layer like SQLCipher (@MASTG-KNOW-0038), anyone who gains access to the app's sandbox (via physical access or backup extraction) can extract the plaintext data. + +This test inspects the app's code for references to Room APIs and verifies whether an encryption layer is applied to the data before being stored using such APIs. + +## Steps + +1. Obtain the application package (APK) using @MASTG-TECH-0003. + +2. Use static analysis (@MASTG-TECH-0014) to identify references to Room APIs: + - `androidx.room.Room` + - `@Database`, `@Dao`, `@Entity` annotations + - `databaseBuilder`, `build` calls, `SupportSQLiteOpenHelper.Factory` implementations + +3. Inspect whether: + - sensitive fields (tokens, secrets, PII) are stored in plaintext within `@Entity` classes + - a secure factory or wrapper (e.g., SQLCipher implementation of `SupportSQLiteOpenHelper.Factory`) is explicitly applied to the database builder + +## Observation + +The output should contain: + +- Which Room database files are referenced in the code +- Whether encryption is being applied in the code to the sensitive data before being stored + +## Evaluation + +The test fails if the app stores sensitive data in Room databases without encryption (e.g., SQLCipher or equivalent) applied.