Skip to content

MASTG-DEMO-0128: Sensitive Data Exposed Through an Unprotected Activity

Download MASTG-DEMO-0128 APK Open MASTG-DEMO-0128 Folder Build MASTG-DEMO-0128 APK

Sample

The sample app performs a login flow by using two activities. Tapping Start in the main screen launches PinEntryActivity, which prompts for a PIN (4321) before proceeding to SecretActivity. SecretActivity displays sensitive account data and is meant to be reachable only after the user passes the PIN check.

However, SecretActivity is declared as exported in the AndroidManifest.xml without an android:permission. This allows third-party apps, or adb, to start SecretActivity directly without interacting with PinEntryActivity, bypassing the PIN gate entirely.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
// SUMMARY: This sample shows a two-activity login flow where PinEntryActivity
// enforces a PIN (4321) before launching SecretActivity. Because SecretActivity
// is exported with no android:permission, an attacker can start it directly with
// adb and bypass the PIN check entirely.

package org.owasp.mastestapp

import android.app.Activity
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.text.InputType
import android.widget.Button
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView

class MastgTest(private val context: Context) {

    fun mastgTest(): String {
        // Launch PinEntryActivity - the intended path that enforces the PIN gate.
        // SecretActivity is also exported, so an attacker can bypass PinEntryActivity
        // entirely by starting it directly.
        context.startActivity(
            Intent(context, PinEntryActivity::class.java).apply {
                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            }
        )
        return "Launching PIN entry screen..."
    }

    // Legitimate entry point: enforces PIN 4321 before launching SecretActivity.
    // Not exported - only reachable through the app's own flow.
    class PinEntryActivity : Activity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            actionBar?.hide()

            val layout = LinearLayout(this).apply {
                orientation = LinearLayout.VERTICAL
                setPadding(64, 120, 64, 64)
            }

            val title = TextView(this).apply {
                text = "MASTestApp - Secure Area"
                textSize = 22f
            }

            val subtitle = TextView(this).apply {
                text = "Enter your PIN to access the secret screen."
                textSize = 16f
                setPadding(0, 24, 0, 48)
            }

            val pinInput = EditText(this).apply {
                hint = "PIN"
                inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD
            }

            val button = Button(this).apply {
                text = "Start"
                setOnClickListener {
                    if (pinInput.text.toString() == "4321") {
                        startActivity(Intent(this@PinEntryActivity, SecretActivity::class.java))
                    } else {
                        AlertDialog.Builder(this@PinEntryActivity)
                            .setTitle("Wrong PIN")
                            .setMessage("Incorrect PIN. Try again.")
                            .setPositiveButton("OK", null)
                            .show()
                    }
                }
            }

            layout.addView(title)
            layout.addView(subtitle)
            layout.addView(pinInput)
            layout.addView(button)
            setContentView(layout)
        }
    }

    // FAIL: [MASTG-TEST-0364] SecretActivity is exported and does not need any special permissions.
    // External callers, like other apps including adb, can start it directly, bypassing PinEntryActivity.
    class SecretActivity : Activity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            actionBar?.hide()

            val secret = buildString {
                append("SECRET SCREEN (reached without authentication)\n\n")
                append("Account: 1234-5678-9012-3456\n")
                append("Balance: 10,000\n")
                append("Recovery PIN: 4321")
            }

            val scrollView = ScrollView(this)
            val view = TextView(this).apply {
                text = secret
                textSize = 28f
                setPadding(48, 48, 48, 48)
            }
            scrollView.addView(view)
            setContentView(scrollView)
        }
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MASTestApp"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:windowSoftInputMode="adjustResize"
            android:theme="@style/Theme.MASTestApp">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name="org.owasp.mastestapp.MastgTest$PinEntryActivity"
            android:exported="false"
            android:theme="@android:style/Theme.Material.Light" />
        <activity
            android:name="org.owasp.mastestapp.MastgTest$SecretActivity"
            android:exported="true"
            android:theme="@android:style/Theme.Material.Light" />
    </application>

</manifest>

Steps

  1. Use Obtaining Information from the AndroidManifest to obtain the AndroidManifest.xml.
  2. Use Enumerating Activities to list the exported activities and their associated android:permission by running run.sh.
run.sh
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/bin/bash
# List the activities declared as exported in the AndroidManifest.
python3 - <<'PY' > output.txt
import re
xml = open("AndroidManifest_reversed.xml").read()
for m in re.finditer(r"<activity\b.*?(?:/>|</activity>)", xml, re.S):
    block = m.group(0)
    name = re.search(r'android:name="([^"]+)"', block)
    exported = re.search(r'android:exported="([^"]+)"', block)
    permission = re.search(r'android:permission="([^"]+)"', block)
    if exported and exported.group(1) == "true":
        print("Exported activity:", name.group(1) if name else "?", "| permission:", permission.group(1) if permission else "none")
PY

Observation

The output reveals the exported activities and their associated permissions:

output.txt
1
2
3
4
Exported activity: org.owasp.mastestapp.MainActivity | permission: none
Exported activity: org.owasp.mastestapp.MastgTest.SecretActivity | permission: none
Exported activity: androidx.compose.ui.tooling.PreviewActivity | permission: none
Exported activity: androidx.activity.ComponentActivity | permission: none

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 activity displays account data in onCreate without checking whether the user completed the PIN challenge:

class SecretActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ... displays account number, balance, and recovery PIN ...
    }
}

The output also lists other exported activities. These are triaged but not reported as vulnerable in this test case.

MainActivity is the launcher activity. Launcher activities normally need to be exported so Android and the launcher can start the app. Android's guidance says activities with the LAUNCHER category should use android:exported="true", while most other components should use false.

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.