Skip to content

MASTG-DEMO-0151: Deep Link Intent Filter Missing android:autoVerify with semgrep

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

Sample

The app handles the https://deeplink.example.com App Link in DeepLinkActivity and performs a sensitive action (a transfer) when the link is opened. The amount parameter is validated correctly (it must be a positive number within an allowed range), so this demo does not depend on an input validation flaw.

The weakness is that the App Link is declared without android:autoVerify="true", so Android never verifies that the app owns the deeplink.example.com domain. Another app can register the same domain to hijack the link, or invoke this exported handler directly, to reach the sensitive action.

MastgTest.kt
 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
package org.owasp.mastestapp

import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle

// SUMMARY: This sample performs a sensitive action (a transfer) through an
// http/https App Link deep link. The input IS validated correctly, so the
// weakness is NOT input validation (that is covered by a separate test). The
// weakness is that the App Link is not verified: the manifest is missing
// android:autoVerify="true", so the OS never confirms that this app owns the
// deeplink.example.com domain. Another app can register the same domain to
// hijack the link (and any sensitive data it carries), or invoke this exported
// handler directly, to reach the sensitive action.

class DeepLinkActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        lastDeepLink = intent?.data
        startActivity(Intent(this, MainActivity::class.java).apply {
            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
        })
        finish()
    }

    companion object {
        // Holds the URI that launched the app via the App Link.
        var lastDeepLink: Uri? = null
    }
}

class MastgTest(private val context: Context) {

    fun mastgTest(): String {
        val data: Uri = DeepLinkActivity.lastDeepLink
            ?: return """
                No deep link processed yet.

                Trigger the unverified App Link with:
                adb shell am start -n org.owasp.mastestapp/.DeepLinkActivity -a android.intent.action.VIEW -d "https://deeplink.example.com/transfer?amount=100"

                Then press Start again to see the result.
            """.trimIndent()

        val amountParam = data.getQueryParameter("amount")

        // The input IS validated correctly: it must be a positive number within
        // an allowed range. This keeps the focus on the App Link verification
        // weakness, not on input validation.
        val amount = amountParam?.toLongOrNull()
        if (amount == null || amount <= 0 || amount > 10_000) {
            return "Rejected invalid amount: $amountParam"
        }

        return processTransfer(amount)
    }

    private fun processTransfer(amount: Long): String {
        // Sensitive action. Even though the amount is validated, this action is
        // reached through an unverified App Link and can be hijacked or
        // triggered by another app on the device.
        return "Transferred $amount units to the linked account"
    }
}

The App Link is declared in the manifest without android:autoVerify="true":

 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
<?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: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=".DeepLinkActivity"
            android:exported="true"
            android:theme="@style/Theme.MASTestApp">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data
                    android:host="deeplink.example.com"
                    android:scheme="http" />
                <data
                    android:host="deeplink.example.com"
                    android:scheme="https" />
            </intent-filter>
        </activity>

        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:theme="@style/Theme.MASTestApp">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>
</manifest>
 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
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    android:versionCode="1"
    android:versionName="1.0"
    android:compileSdkVersion="35"
    android:compileSdkVersionCodename="15"
    package="org.owasp.mastestapp"
    platformBuildVersionCode="35"
    platformBuildVersionName="15">
    <uses-sdk
        android:minSdkVersion="29"
        android:targetSdkVersion="35"/>
    <uses-permission android:name="android.permission.INTERNET"/>
    <permission
        android:name="org.owasp.mastestapp.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
        android:protectionLevel="signature"/>
    <uses-permission android:name="org.owasp.mastestapp.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"/>
    <application
        android:theme="@style/Theme.MASTestApp"
        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"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:appComponentFactory="androidx.core.app.CoreComponentFactory">
        <activity
            android:theme="@style/Theme.MASTestApp"
            android:name="org.owasp.mastestapp.DeepLinkActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>
                <data
                    android:scheme="http"
                    android:host="deeplink.example.com"/>
                <data
                    android:scheme="https"
                    android:host="deeplink.example.com"/>
            </intent-filter>
        </activity>
        <activity
            android:theme="@style/Theme.MASTestApp"
            android:name="org.owasp.mastestapp.MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <activity
            android:name="androidx.compose.ui.tooling.PreviewActivity"
            android:exported="true"/>
        <activity
            android:theme="@android:style/Theme.Material.Light.NoActionBar"
            android:name="androidx.activity.ComponentActivity"
            android:exported="true"/>
        <provider
            android:name="androidx.startup.InitializationProvider"
            android:exported="false"
            android:authorities="org.owasp.mastestapp.androidx-startup">
            <meta-data
                android:name="androidx.emoji2.text.EmojiCompatInitializer"
                android:value="androidx.startup"/>
            <meta-data
                android:name="androidx.lifecycle.ProcessLifecycleInitializer"
                android:value="androidx.startup"/>
            <meta-data
                android:name="androidx.profileinstaller.ProfileInstallerInitializer"
                android:value="androidx.startup"/>
        </provider>
        <receiver
            android:name="androidx.profileinstaller.ProfileInstallReceiver"
            android:permission="android.permission.DUMP"
            android:enabled="true"
            android:exported="true"
            android:directBootAware="false">
            <intent-filter>
                <action android:name="androidx.profileinstaller.action.INSTALL_PROFILE"/>
            </intent-filter>
            <intent-filter>
                <action android:name="androidx.profileinstaller.action.SKIP_FILE"/>
            </intent-filter>
            <intent-filter>
                <action android:name="androidx.profileinstaller.action.SAVE_PROFILE"/>
            </intent-filter>
            <intent-filter>
                <action android:name="androidx.profileinstaller.action.BENCHMARK_OPERATION"/>
            </intent-filter>
        </receiver>
    </application>
</manifest>

Steps

Let's run our semgrep rule against the reversed manifest.

../../../../rules/mastg-android-deeplink-autoverify-missing.yml
 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
rules:
- id: mastg-android-deeplink-autoverify-missing
  severity: WARNING
  languages:
    - xml
  metadata:
    summary: This rule looks for http/https deep links declared without App Links verification.
  message: '[MASVS-PLATFORM] Deep link intent filter missing android:autoVerify="true", enabling malicious apps to hijack links.'
  patterns:
    - pattern: <data ... android:scheme="$SCHEME" ... />
    - metavariable-regex:
        metavariable: $SCHEME
        regex: ^https?$
    - pattern-inside: |
        <activity ...>
          ...
        </activity>
    - pattern-inside: |
        <intent-filter ...>
          ...
          <action android:name="android.intent.action.VIEW" />
          ...
        </intent-filter>
    - pattern-inside: |
        <intent-filter ...>
          ...
          <category android:name="android.intent.category.BROWSABLE" />
          ...
        </intent-filter>
    - pattern-not-inside: |
        <intent-filter ... android:autoVerify="true" ...>
          ...
        </intent-filter>
run.sh
1
NO_COLOR=true semgrep -c ../../../../rules/mastg-android-deeplink-autoverify-missing.yml ./AndroidManifest_reversed.xml --text > output.txt

Observation

The rule flagged the http and https <data> declarations in the DeepLinkActivity <intent-filter>, which is missing the android:autoVerify="true" attribute.

output.txt
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
┌─────────────────┐
 2 Code Findings 
└─────────────────┘

    AndroidManifest_reversed.xml
    ❯❱ rules.mastg-android-deeplink-autoverify-missing
          [MASVS-PLATFORM] Deep link intent filter missing android:autoVerify="true", enabling malicious apps
          to hijack links.                                                                                   

           37 <data
           38     android:scheme="http"
           39     android:host="deeplink.example.com"/>
            ⋮┆----------------------------------------
           40 <data
           41     android:scheme="https"
           42     android:host="deeplink.example.com"/>

Evaluation

The test case fails because the app declares an http/https App Link without enabling verification. Without android:autoVerify="true", Android does not confirm that the app owns the deeplink.example.com domain, so the app is not its exclusive, verified handler.

Note that the handler validates its input correctly — the issue is not the parameter handling, but that the link itself is unverified. Because the domain is not verified, a malicious app can register the same https://deeplink.example.com intent filter and intercept the deep link (and any sensitive data it carries) or present spoofed content. Since the handler is also exported, any app on the device can invoke it directly to trigger the sensitive action. Use adb to confirm the action is reachable:

adb shell am start -n org.owasp.mastestapp/.DeepLinkActivity -a android.intent.action.VIEW -d "https://deeplink.example.com/transfer?amount=100"

After triggering the deep link, tap Start in the app to process it. The result (Transferred 100 units to the linked account) confirms that the sensitive action is reachable through the unverified App Link.