The app registers the custom URL scheme mastestapp://transfer on DeepLinkActivity. Any app on the device can open it, for example with mastestapp://transfer?amount=9999999.
The deep link handler entry point is DeepLinkActivity.onCreate, which reads the incoming URI with getIntent().getData() (reverse engineered as DeepLinkActivity_reversed.java). The URI is then processed in MastgTest, which extracts the amount query parameter with getQueryParameter("amount") and passes it directly to processTransfer() as a raw String, without calling toLong() or performing any bounds or range check.
packageorg.owasp.mastestappimportandroid.app.Activityimportandroid.content.Contextimportandroid.content.Intentimportandroid.net.Uriimportandroid.os.Bundle// SUMMARY: This sample demonstrates a custom URL scheme handler that uses a URL// query parameter directly without validating it (no numeric type conversion// and no bounds checking)./** * Receives the custom URL scheme `mastestapp://transfer`. Any app on the device * can launch it, for example with: * * adb shell am start -a android.intent.action.VIEW -d "mastestapp://transfer?amount=9999999" * * It remembers the launching URI and routes the user to MainActivity so the * deep link can be processed by tapping the Start button. */classDeepLinkActivity:Activity(){overridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)lastDeepLink=intent?.datastartActivity(Intent(this,MainActivity::class.java).apply{addFlags(Intent.FLAG_ACTIVITY_NEW_TASKorIntent.FLAG_ACTIVITY_CLEAR_TOP)})finish()}companionobject{// Holds the URI that launched the app via the custom URL scheme.varlastDeepLink:Uri? =null}}classMastgTest(privatevalcontext:Context){funmastgTest():String{valdata:Uri=DeepLinkActivity.lastDeepLink?:return""" No deep link processed yet. Trigger the vulnerable custom URL scheme handler with: adb shell am start -a android.intent.action.VIEW -d "mastestapp://transfer?amount=9999999" Then press Start again to see the result. """.trimIndent()valamount=data.getQueryParameter("amount")?:return"Missing 'amount' parameter"// FAIL: [MASTG-TEST-0394] The handler uses the "amount" query parameter// directly without converting it to a numeric type (e.g. toLong()) or// checking it against any bounds. Any app can open// mastestapp://transfer?amount=9999999 or amount=-1 to bypass the// expected business logic constraints.returnprocessTransfer(amount)}privatefunprocessTransfer(amount:String):String{return"Transferring $amount units"}}
packageorg.owasp.mastestapp;importandroid.content.Context;importandroid.net.Uri;importkotlin.Metadata;importkotlin.jvm.internal.Intrinsics;/* 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\u0001B\u000f\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0004\b\u0004\u0010\u0005J\u0006\u0010\u0006\u001a\u00020\u0007J\u0010\u0010\b\u001a\u00020\u00072\u0006\u0010\t\u001a\u00020\u0007H\u0002R\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","","processTransfer","amount","app_debug"},k=1,mv={2,0,0},xi=48)publicfinalclassMastgTest{publicstaticfinalint$stable=8;privatefinalContextcontext;publicMastgTest(Contextcontext){Intrinsics.checkNotNullParameter(context,"context");this.context=context;}publicfinalStringmastgTest(){Uridata=DeepLinkActivity.INSTANCE.getLastDeepLink();if(data==null){return"No deep link processed yet.\n\nTrigger the vulnerable custom URL scheme handler with:\nadb shell am start -a android.intent.action.VIEW -d \"mastestapp://transfer?amount=9999999\"\n\nThen press Start again to see the result.";}Stringamount=data.getQueryParameter("amount");if(amount==null){return"Missing 'amount' parameter";}returnprocessTransfer(amount);}privatefinalStringprocessTransfer(Stringamount){return"Transferring "+amount+" units";}}
The custom URL scheme is declared in the manifest:
Run semgrep with two rules. The first rule scans the reversed manifest to locate the custom URL scheme declarations and the activity that handles them. The second rule scans the reverse-engineered handler code to locate the deep link handler entry point (getIntent().getData()) and where the URI parameters are read (getQueryParameter), so they can be reviewed for validation (see Reviewing Decompiled Java Code).
rules:-id:mastg-android-custom-deeplink-schemeseverity:INFOlanguages:-xmlmetadata:summary:Thisrulelooksforcustom(nonhttp/https)URLschemedeeplinksdeclaredintheAndroidManifest.message:'[MASVS-PLATFORM] Custom deeplink scheme detected in an intent-filter (potentially exposed deeplink). Any app on the device can invoke this handler.'patterns:-pattern:<data...android:scheme="$SCHEME".../>-metavariable-regex:metavariable:$SCHEMEregex:^(?!https?$).+$-pattern-inside:|<activity...>...</activity>-pattern-inside:|<intent-filter...>...<actionandroid:name="android.intent.action.VIEW"/>...</intent-filter>-pattern-inside:|<intent-filter...>...<categoryandroid:name="android.intent.category.BROWSABLE"/>...</intent-filter>
rules:-id:mastg-android-deeplink-unvalidated-parameterseverity:WARNINGlanguages:-javametadata:summary:ThisrulelocatesthedeeplinkhandlerentrypointandtheURIparametersitreads.message:'[MASVS-PLATFORM] A deep link handler reads the incoming URI (getIntent/getData) and/or extracts a parameter (getQueryParameter). Verify each value is validated (type conversion, bounds, or sanitization) before it is used.'pattern-either:-pattern:$INTENT.getData()-pattern:$URI.getQueryParameter(...)
The first rule identified the mastestapp://transfer custom URL scheme declared on DeepLinkActivity, confirming that any app on the device can reach this handler. The second rule pointed to the handler entry point in DeepLinkActivity_reversed.java (the reverse engineered DeepLinkActivity), where the incoming URI is read with getIntent().getData() (line 24), and to MastgTest_reversed.java, where the amount query parameter is extracted with getQueryParameter("amount") (line 25).
Following the URI from the handler entry point, DeepLinkActivity (DeepLinkActivity_reversed.java) reads it with getIntent().getData() and the amount parameter is later extracted with getQueryParameter("amount") in MastgTest_reversed.java, then passed straight to processTransfer() without validation.
The test case fails because the handler uses the amount query parameter directly, without validating it. Analyzing the handler method confirms this:
Stringamount=data.getQueryParameter("amount");// raw String from the URI...returnprocessTransfer(amount);// used as-is...privatefinalStringprocessTransfer(Stringamount){return"Transferring "+amount+" units";// no toLong(), no bounds check}
The value is never:
Converted to a numeric type (e.g., toLong()).
Checked against an expected range.
Any app can therefore trigger mastestapp://transfer?amount=9999999 or supply a non-numeric value to bypass the expected business logic constraints. Use adb to confirm:
After triggering the deep link, tap Start in the app to process it. The result (for example, Transferring 9999999 units) confirms that the handler accepts the attacker-controlled value and uses it without numeric conversion or bounds checking.