Android Deeplink Hijacking: What Bug Bounty Hunters Should Prove
Deeplinks are convenient until another app on the same device can open the same URI and read what was never meant for it. On Android that usually means an exported Activity, a broad intent filter, and a sensitive parameter sitting in the path or query string.
Stop treating "opens in another app" as the whole finding. Triage wants a concrete effect: a reset token landed in your Activity, a session cookie was forwarded, or an authenticated action ran without re-auth.
Pull the surface from the APK
Decompile the target and list intent filters under android.intent.action.VIEW with http, https, or a custom scheme. Note host, pathPrefix, pathPattern, and whether android:autoVerify is set for App Links. Custom schemes (myapp://) are the softest target because any app can claim them.
Look for handlers that parse getIntent().getData() and then:
- Attach a token from the URI to a WebView or Retrofit call
- Auto-login with a one-time code
- Navigate to
/checkout,/transfer, or/oauth/callback - Load an arbitrary URL from a query parameter into a WebView
Exported equals reachable. If android:exported="true" (or the pre-Android 12 default for filters with intents) and no permission guards the Activity, your proof app can send the same Intent. Also check BROWSABLE category presence—without it, some browser-originated links never reach the app, which changes exploitability from a phishing email versus an installed malicious app.
Prove hijack with an owned receiver
Install a minimal second app that registers the same scheme or a colliding unverified http link. Trigger the sensitive flow from email, push, or adb shell am start with a canary token you generated. If your Activity receives token=bf_canary_9182 and the legitimate app never sees it, you have theft—not a theoretical race.
App Links with successful digital asset links verification are harder. Verification failure, mismatched sha256_cert_fingerprints, or a path the assetlinks.json never claimed can fall back to a chooser or another handler. Document the verification state; do not invent a bypass you did not observe.
WebView bridges amplify impact. A hijacked link that lands in a WebView with JavaScript interfaces or cookie sharing can become XSS-adjacent. Keep the report honest: show the stolen parameter first, then any secondary sink. If the Activity forwards the full URI into loadUrl() without an allowlist, say so as a second chained issue rather than inflating the deeplink claim alone.
Intent extras and confused deputies
Not every bug is the URI itself. Some apps accept Intent extras that override the deeplink path after the filter matched. Send a VIEW intent that satisfies the filter, then add extras the code reads with higher priority than the URI. I've seen a verified https link open the right Activity while an extra named redirect still pointed the WebView at an attacker host.
Task affinity and singleTask launch modes change whether your malicious app stays in front after stealing the link. Mention the observed UX: silent steal, disambiguation dialog, or brief flash of the real app. Reproducibility notes save you from "could not reproduce" loops when triage uses a different Android version.
What not to file
A custom scheme without a secret in the URI is low or informational. A verified Android App Link that only your package opens is working as designed. Do not test by phishing real users; use accounts and tokens you control. Avoid claiming "any installed app can steal OAuth" unless you showed the authorization code or access token in your handler logs.
Recommend verified App Links for https, reject unexpected hosts, bind one-time tokens to the installing app attestation where feasible, and never put long-lived session material in a deeplink. Prefer server-side exchange: the link carries a single-use reference; the app redeems it with an authenticated API call. A short PoC APK, the exact Intent, and the canary in your logs beats a wall of Smali screenshots.