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.
// 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.packageorg.owasp.mastestappimportandroid.app.Activityimportandroid.app.AlertDialogimportandroid.content.Contextimportandroid.content.Intentimportandroid.os.Bundleimportandroid.text.InputTypeimportandroid.widget.Buttonimportandroid.widget.EditTextimportandroid.widget.LinearLayoutimportandroid.widget.ScrollViewimportandroid.widget.TextViewclassMastgTest(privatevalcontext:Context){funmastgTest():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.classPinEntryActivity:Activity(){overridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)actionBar?.hide()vallayout=LinearLayout(this).apply{orientation=LinearLayout.VERTICALsetPadding(64,120,64,64)}valtitle=TextView(this).apply{text="MASTestApp - Secure Area"textSize=22f}valsubtitle=TextView(this).apply{text="Enter your PIN to access the secret screen."textSize=16fsetPadding(0,24,0,48)}valpinInput=EditText(this).apply{hint="PIN"inputType=InputType.TYPE_CLASS_NUMBERorInputType.TYPE_NUMBER_VARIATION_PASSWORD}valbutton=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.classSecretActivity:Activity(){overridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)actionBar?.hide()valsecret=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")}valscrollView=ScrollView(this)valview=TextView(this).apply{text=secrettextSize=28fsetPadding(48,48,48,48)}scrollView.addView(view)setContentView(scrollView)}}}
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 910111213
#!/bin/bash# List the activities declared as exported in the AndroidManifest.python3-<<'PY'>output.txtimportrexml=open("AndroidManifest_reversed.xml").read()forminre.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)ifexportedandexported.group(1)=="true":print("Exported activity:",name.group(1)ifnameelse"?","| permission:",permission.group(1)ifpermissionelse"none")PY
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:
classSecretActivity:Activity(){overridefunonCreate(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.