-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Add Tests and Demos about "Sensitive Data Stored Unencrypted via Room" #3541
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
a7c7473
4e7e048
bd67b52
503675c
2f30132
3f1d8e1
cbb8444
3b3cd99
834c6e4
ad1a20d
fde6102
3332feb
076c0ba
c06b7ba
7909da1
ecbe9e7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<UserEntity> | ||
| } | ||
|
|
||
| @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." | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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;", "<init>", "(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."; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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") | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| kotlin("kapt") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| --- OBSERVATION: Unencrypted Room Database Content --- | ||
| 1|demoUser_room|john.doe@maswe.com|ghp_1234567890abcdefghijklmnopqrstuvABCD |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| #!/bin/bash | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The DEMO executes a run.sh file that extracts the Room database from the app's sandbox, but this is not what is made in the test. Normally, run.sh is a script that automates parts of the test (for example, if the tests consists on hooking several functions, the frida command is attached here for better reproducibility). Please also update run.sh accordingly to the TEST steps when updating the DEMO. |
||
|
|
||
| # 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." | ||
|
macik09 marked this conversation as resolved.
|
||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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] | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. BEST-0x25 is a new BEST file? Maybe you can make a BEST file for Android that recommends Storing Data Encrypted in App Sandbox Directory, like the one that exists for iOS right now (https://mas.owasp.org/MASTG/best-practices/MASTG-BEST-0024/). You can attach this BEST file here, or in the other PRs, and expand it with examples of each of the ways to store data (Room, SQLite, DataStore) - If you are not doing this now, please create a PR to keep track of the needs of creating a file like this for Android.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Best 0x25 is part of PR #3534 |
||
| 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/<package_name>/databases/<database_name>` | ||
| - `/data/data/<package_name>/databases/<database_name>-wal` | ||
| - `/data/data/<package_name>/databases/<database_name>-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. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @cpholguera Should this be in a specific, little KNOW file talking about Room?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also Room is a SQLite wrapper, I don't know if that serves a mention (there is already a KNOW file of SQLite: MASTG-KNOW-0037, https://mas.owasp.org/MASTG/knowledge/android/MASVS-STORAGE/MASTG-KNOW-0037/)
macik09 marked this conversation as resolved.
|
||
|
|
||
| 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. | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The DEMO must follow the content guidelines (https://mas.owasp.org/contributing/writing-content/mastg-demo.instructions).
For example, the "Evaluation" section should explain each relevant line of the output that is used as an evidence to give a FAIL to the test.