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
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:debuggable="true"
android:testOnly="true"
android:allowBackup="true"
android:supportsRtl="true"
android:extractNativeLibs="false"
Expand Down
106 changes: 96 additions & 10 deletions demos/android/MASVS-PLATFORM/MASTG-DEMO-0128/MASTG-DEMO-0128.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,41 @@

## Steps

1. Use @MASTG-TECH-0117 to obtain the AndroidManifest.xml.
2. Use @MASTG-TECH-0160 to list the exported activities and their associated `android:permission` by running `run.sh`.
1. Use @MASTG-TECH-0013 to reverse engineer the app and @MASTG-TECH-0117 to obtain the `AndroidManifest.xml`.
2. Use @MASTG-TECH-0160 to enumerate the activities, their export state, and any associated `android:permission`. `run.sh` does this by running @MASTG-TOOL-0110 in its "Stage 1" with the following rule, which flags activities that are exported without a permission and lists the permission-protected ones separately:

{{ ../../../../rules/mastg-android-exported-activity.yml }}

3. Use @MASTG-TECH-0014 to inspect the decompiled code of each exported activity. `run.sh` runs @MASTG-TOOL-0110 in its "Stage 2" with the following rule to locate the lifecycle entry points reachable when the activity is started (`onCreate`, `onStart`, `onResume`, `onNewIntent`):

Check failure on line 25 in demos/android/MASVS-PLATFORM/MASTG-DEMO-0128/MASTG-DEMO-0128.md

View workflow job for this annotation

GitHub Actions / markdown-lint-check

Ordered list item prefix

demos/android/MASVS-PLATFORM/MASTG-DEMO-0128/MASTG-DEMO-0128.md:25:1 MD029/ol-prefix Ordered list item prefix [Expected: 1; Actual: 3; Style: 1/1/1] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md029.md

{{ ../../../../rules/mastg-android-activity-entrypoints.yml }}

{{ run.sh }}

4. Run `evaluate.sh` to reduce the exported, unprotected activities from "Stage 1" to the ones the app itself declares. Activities in framework or library namespaces (`android.*`, `androidx.*`, `com.google.android.*`) ship with dependencies rather than the app's own code, so they are triaged separately:

Check failure on line 31 in demos/android/MASVS-PLATFORM/MASTG-DEMO-0128/MASTG-DEMO-0128.md

View workflow job for this annotation

GitHub Actions / markdown-lint-check

Ordered list item prefix

demos/android/MASVS-PLATFORM/MASTG-DEMO-0128/MASTG-DEMO-0128.md:31:1 MD029/ol-prefix Ordered list item prefix [Expected: 1; Actual: 4; Style: 1/1/1] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md029.md

{{ evaluate.sh }}

## Observation

The output reveals the exported activities and their associated permissions:
"Stage 1" lists the exported activities and their permissions, and "Stage 2" points at the activity code to review:

{{ output.txt }}
{{ manifest_scan.txt # code_scan.txt }}

`evaluate.sh` narrows the exported, unprotected activities down to the app's own components:

{{ evaluation.txt }}

## Evaluation

The test case fails because `SecretActivity` exposes sensitive functionality and is exported (`android:exported="true"`) without any permission protection, so external callers can start it directly by using an explicit intent.

`PinEntryActivity` does not protect the underlying exported activity; access control must be enforced at the `SecretActivity` boundary.
The "Stage 1" scan reports four exported, unprotected activities, and we discard three of them:

The activity displays account data in `onCreate` without checking whether the user completed the PIN challenge:
- `androidx.compose.ui.tooling.PreviewActivity` is a Compose tooling activity used by Android Studio to run composable previews, and `androidx.activity.ComponentActivity` is a generic host activity added by the Compose libraries. Both live in the `androidx.*` namespace, so `evaluate.sh` drops them as library code; they are not part of the app's authentication flow and should only be reviewed if the tested build is meant for production.
- `MainActivity` is app-owned, so it survives the namespace filter and remains in `evaluation.txt`, but the "Stage 1" scan shows it carries the `LAUNCHER` intent filter. Launcher activities must be exported so Android and the launcher can start the app (see [Android's guidance](https://developer.android.com/about/versions/12/behavior-changes-12#exported)), and its `onCreate` only shows the app's entry screen, so it is discarded after manual review.

`org.owasp.mastestapp.MastgTest$SecretActivity` is the remaining app-owned activity. Its `onCreate` displays sensitive account data without checking whether the user completed the PIN challenge.

```kotlin
class SecretActivity : Activity() {
Expand All @@ -45,10 +62,79 @@
}
```

The output also lists other exported activities. These are triaged but not reported as vulnerable in this test case.
### 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.

`MainActivity` is the launcher activity. Launcher activities normally need to be exported so Android and the launcher can start the app. [Android's guidance](https://developer.android.com/about/versions/12/behavior-changes-12#exported) says activities with the `LAUNCHER` category should use `android:exported="true"`, while most other components should use `false`.
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
```

`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.
**Why not rely solely on the PIN check in the calling activity?:**

`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.
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.
120 changes: 120 additions & 0 deletions demos/android/MASVS-PLATFORM/MASTG-DEMO-0128/MastgTest_reversed.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package org.owasp.mastestapp;

import android.app.ActionBar;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import androidx.compose.material.MenuKt;
import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
import org.owasp.mastestapp.MastgTest;

/* JADX INFO: compiled from: MastgTest.kt */
/* JADX INFO: loaded from: classes3.dex */
@Metadata(d1 = {"\u0000\u001a\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0010\u000e\n\u0002\b\u0003\b\u0007\u0018\u00002\u00020\u0001:\u0002\b\tB\u000f\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0004\b\u0004\u0010\u0005J\u0006\u0010\u0006\u001a\u00020\u0007R\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", "mastgTest", "", "PinEntryActivity", "SecretActivity", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48)
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;
}

public final String mastgTest() {
Context context = this.context;
Intent $this$mastgTest_u24lambda_u240 = new Intent(this.context, (Class<?>) PinEntryActivity.class);
$this$mastgTest_u24lambda_u240.addFlags(268435456);
context.startActivity($this$mastgTest_u24lambda_u240);
return "Launching PIN entry screen...";
}

/* JADX INFO: compiled from: MastgTest.kt */
@Metadata(d1 = {"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\b\u0007\u0018\u00002\u00020\u0001B\t\b\u0007¢\u0006\u0004\b\u0002\u0010\u0003J\u0012\u0010\u0004\u001a\u00020\u00052\b\u0010\u0006\u001a\u0004\u0018\u00010\u0007H\u0014¨\u0006\b"}, d2 = {"Lorg/owasp/mastestapp/MastgTest$PinEntryActivity;", "Landroid/app/Activity;", "<init>", "()V", "onCreate", "", "savedInstanceState", "Landroid/os/Bundle;", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48)
public static final class PinEntryActivity extends Activity {
public static final int $stable = 0;

@Override // android.app.Activity
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActionBar actionBar = getActionBar();
if (actionBar != null) {
actionBar.hide();
}
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(1);
layout.setPadding(64, MenuKt.InTransitionDuration, 64, 64);
TextView $this$onCreate_u24lambda_u241 = new TextView(this);
$this$onCreate_u24lambda_u241.setText("MASTestApp - Secure Area");
$this$onCreate_u24lambda_u241.setTextSize(22.0f);
TextView $this$onCreate_u24lambda_u242 = new TextView(this);
$this$onCreate_u24lambda_u242.setText("Enter your PIN to access the secret screen.");
$this$onCreate_u24lambda_u242.setTextSize(16.0f);
$this$onCreate_u24lambda_u242.setPadding(0, 24, 0, 48);
final EditText $this$onCreate_u24lambda_u243 = new EditText(this);
$this$onCreate_u24lambda_u243.setHint("PIN");
$this$onCreate_u24lambda_u243.setInputType(18);
Button $this$onCreate_u24lambda_u245 = new Button(this);
$this$onCreate_u24lambda_u245.setText("Start");
$this$onCreate_u24lambda_u245.setOnClickListener(new View.OnClickListener() { // from class: org.owasp.mastestapp.MastgTest$PinEntryActivity$$ExternalSyntheticLambda0
@Override // android.view.View.OnClickListener
public final void onClick(View view) {
MastgTest.PinEntryActivity.onCreate$lambda$5$lambda$4($this$onCreate_u24lambda_u243, this, view);
}
});
layout.addView($this$onCreate_u24lambda_u241);
layout.addView($this$onCreate_u24lambda_u242);
layout.addView($this$onCreate_u24lambda_u243);
layout.addView($this$onCreate_u24lambda_u245);
setContentView(layout);
}

/* JADX INFO: Access modifiers changed from: private */
public static final void onCreate$lambda$5$lambda$4(EditText pinInput, PinEntryActivity this$0, View it) {
Intrinsics.checkNotNullParameter(pinInput, "$pinInput");
Intrinsics.checkNotNullParameter(this$0, "this$0");
if (Intrinsics.areEqual(pinInput.getText().toString(), "4321")) {
this$0.startActivity(new Intent(this$0, (Class<?>) SecretActivity.class));
} else {
new AlertDialog.Builder(this$0).setTitle("Wrong PIN").setMessage("Incorrect PIN. Try again.").setPositiveButton("OK", (DialogInterface.OnClickListener) null).show();
}
}
}

/* JADX INFO: compiled from: MastgTest.kt */
@Metadata(d1 = {"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\b\u0007\u0018\u00002\u00020\u0001B\t\b\u0007¢\u0006\u0004\b\u0002\u0010\u0003J\u0012\u0010\u0004\u001a\u00020\u00052\b\u0010\u0006\u001a\u0004\u0018\u00010\u0007H\u0014¨\u0006\b"}, d2 = {"Lorg/owasp/mastestapp/MastgTest$SecretActivity;", "Landroid/app/Activity;", "<init>", "()V", "onCreate", "", "savedInstanceState", "Landroid/os/Bundle;", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48)
public static final class SecretActivity extends Activity {
public static final int $stable = 0;

@Override // android.app.Activity
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActionBar actionBar = getActionBar();
if (actionBar != null) {
actionBar.hide();
}
StringBuilder $this$onCreate_u24lambda_u240 = new StringBuilder();
$this$onCreate_u24lambda_u240.append("SECRET SCREEN (reached without authentication)\n\n");
$this$onCreate_u24lambda_u240.append("Account: 1234-5678-9012-3456\n");
$this$onCreate_u24lambda_u240.append("Balance: 10,000\n");
$this$onCreate_u24lambda_u240.append("Recovery PIN: 4321");
String secret = $this$onCreate_u24lambda_u240.toString();
ScrollView scrollView = new ScrollView(this);
TextView $this$onCreate_u24lambda_u241 = new TextView(this);
$this$onCreate_u24lambda_u241.setText(secret);
$this$onCreate_u24lambda_u241.setTextSize(28.0f);
$this$onCreate_u24lambda_u241.setPadding(48, 48, 48, 48);
scrollView.addView($this$onCreate_u24lambda_u241);
setContentView(scrollView);
}
}
}
Loading
Loading