Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 46 additions & 3 deletions demos/android/MASVS-STORAGE/MASTG-DEMO-0070/MASTG-DEMO-0070.md

@jacobocasado jacobocasado May 2, 2026

Copy link
Copy Markdown
Collaborator

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.

Original file line number Diff line number Diff line change
Expand Up @@ -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.
103 changes: 103 additions & 0 deletions demos/android/MASVS-STORAGE/MASTG-DEMO-0070/MastgTest.kt
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")
2 changes: 2 additions & 0 deletions demos/android/MASVS-STORAGE/MASTG-DEMO-0070/output.txt
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
38 changes: 38 additions & 0 deletions demos/android/MASVS-STORAGE/MASTG-DEMO-0070/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/bin/bash

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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."
Comment thread
macik09 marked this conversation as resolved.
11 changes: 0 additions & 11 deletions tests-beta/android/MASVS-STORAGE/MASTG-TEST-0306.md

This file was deleted.

42 changes: 42 additions & 0 deletions tests-beta/android/MASVS-STORAGE/MASTG-TEST-0x01.md
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]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.
42 changes: 42 additions & 0 deletions tests-beta/android/MASVS-STORAGE/MASTG-TEST-0x02.md
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.

@jacobocasado jacobocasado May 21, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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/)

Comment thread
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.
Loading