
Deep links let mobile applications open directly to specific screens or trigger specific actions from a URL, whether that link comes from a website, an email, another app, or a push notification. This convenience comes with a real attack surface. When deep link handling is implemented without proper validation, attackers can hijack authentication flows, bypass access controls, inject malicious data, or redirect users to phishing pages, all while appearing to originate from a legitimate app. This post walks through how deep link exploitation actually works and what a secure implementation looks like.
Deep links come in two main forms. Custom URL schemes, such as myapp://, register an app to handle a specific scheme at the operating system level. Android App Links and iOS Universal Links go a step further, binding a standard HTTPS URL to an app if the app is installed and verified as the legitimate owner of that domain.
The security risk arises because deep link handling code often trusts the incoming URL more than it should. Any app, or any website, can construct and send a URL matching a custom scheme, and the receiving app has no inherent way to know whether that URL came from a trusted source or a malicious one unless it validates explicitly.
Mobile applications handling authentication tokens, payment flows, or sensitive account actions through deep links are common targets for this class of vulnerability. Teams shipping mobile apps at scale often bring in a dedicated mobile assessment before launch. Sagefox Cybersecurity performs mobile application security reviews that specifically test deep link and intent handling as part of a broader mobile assessment.
On Android, multiple apps can register the same custom URL scheme unless additional verification steps are taken. If a malicious app registers the same scheme as a legitimate banking app, the operating system may present a chooser, or in some misconfigurations, the malicious app could intercept the link entirely.
xml
<!-- Malicious app's manifest, registering the same scheme as a target app -->
<activity android:name=".PhishingActivity" 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="targetapp" android:host="auth" />
</intent-filter>
</activity>If the legitimate app does not use Android App Links with domain verification, this kind of scheme collision is straightforward to set up and can be used to intercept sensitive data such as OAuth redirect tokens.
A common real-world pattern involves OAuth authorization flows that redirect back to the app via a custom URL scheme containing the authorization code or access token directly in the URL.
text
targetapp://oauth/callback?code=AUTH_CODE_HERE&state=xyz
If a malicious app has registered the same scheme, it can receive this callback instead of, or in addition to, the legitimate app, capturing the authorization code and exchanging it for an access token before the real app ever sees it.
python
import requests
stolen_code = "AUTH_CODE_HERE"
response = requests.post("https://api.targetapp.com/oauth/token", data={
"grant_type": "authorization_code",
"code": stolen_code,
"client_id": "known_public_client_id",
"redirect_uri": "targetapp://oauth/callback"
})
print(response.json())This exact pattern has been documented in real mobile OAuth implementations that relied on custom schemes without PKCE or additional binding between the request and the response.
Deep links frequently pass parameters that control in-app navigation or behavior. When these parameters are trusted without validation, they can be manipulated to reach unintended states.
text
myapp://resetpassword?userId=12345&token=abc123If the app does not validate that the token actually belongs to the specified userId server-side, an attacker could attempt to manipulate the userId parameter directly:
text
myapp://resetpassword?userId=1&token=abc123
Testing for this class of issue involves enumerating parameter combinations against the backend validation logic rather than assuming the client-side deep link data is trustworthy.
bash
# Testing deep link parameter handling using adb on a physical or emulated device
adb shell am start -W -a android.intent.action.VIEW \
-d "myapp://resetpassword?userId=1&token=abc123"Some apps pass deep link parameters directly into an embedded WebView without sanitization, opening the door to injection.
text
myapp://loadpage?url=javascript:alert(document.cookie)If the app's WebView handler loads the url parameter without validating the scheme, this can lead to arbitrary JavaScript execution within the WebView's context, potentially exposing session cookies or other sensitive data accessible to the page.
java
// Vulnerable handling pattern
String targetUrl = intent.getData().getQueryParameter("url");
webView.loadUrl(targetUrl);For Android, decompiling the APK and reviewing the manifest reveals every registered scheme and intent filter.
bash
# Decode the manifest from an APK for review
apktool d target_app.apk -o decoded_app
cat decoded_app/AndroidManifest.xml | grep -A 5 "intent-filter"For iOS, reviewing the app's Info.plist reveals registered URL schemes and Universal Link associated domains.
bash
# Extract and inspect Info.plist from an IPA
unzip -o target_app.ipa -d extracted_ipa
plutil -convert xml1 extracted_ipa/Payload/*.app/Info.plist -o - | grep -A 10 "CFBundleURLSchemes"Once schemes and parameters are identified, systematically testing parameter manipulation, injection payloads, and boundary values helps uncover validation gaps.
python
test_urls = [
"myapp://resetpassword?userId=1&token=abc",
"myapp://resetpassword?userId=' OR '1'='1&token=abc",
"myapp://loadpage?url=javascript:alert(1)",
"myapp://loadpage?url=file:///data/data/com.target.app/shared_prefs/",
"myapp://profile?userId=../../../admin",
]
for url in test_urls:
print(f"Testing: {url}")
# Trigger via adb or device automation framework, then inspect app stateAndroid App Links and iOS Universal Links bind a deep link to a specific domain that the app owner controls, and the operating system verifies this ownership through a hosted association file, closing the scheme collision gap that plain custom schemes leave open.
json
// assetlinks.json hosted at https://targetapp.com/.well-known/assetlinks.json
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.targetapp.android",
"sha256_cert_fingerprints": ["AA:BB:CC:..."]
}
}]json
// apple-app-site-association hosted at https://targetapp.com/.well-known/apple-app-site-association
{
"applinks": {
"apps": [],
"details": [{
"appID": "TEAMID.com.targetapp.ios",
"paths": ["/oauth/*", "/resetpassword/*"]
}]
}
}Any sensitive action triggered by a deep link, such as a password reset or account action, must be validated server-side against the authenticated session or a cryptographically bound token, never based solely on parameters present in the URL.
python
def handle_reset_password(request):
token = request.params.get("token")
record = lookup_token_record(token)
if not record or record.expired or record.used:
return error_response("Invalid or expired token")
# Bind the action to the token's associated user, never a client-supplied userId
user = record.user
process_password_reset(user)Proof Key for Code Exchange binds the authorization request to the specific client that initiated it, preventing a malicious app that intercepts the callback from being able to exchange the code for a token even if it captures the redirect.
python
import hashlib
import base64
import secrets
code_verifier = secrets.token_urlsafe(64)
code_challenge = base64.urlsafe_b64encode(
hashlib.sha256(code_verifier.encode()).digest()
).decode().rstrip("=")
auth_url = (
f"https://auth.targetapp.com/authorize"
f"?client_id=public_client_id"
f"&redirect_uri=targetapp://oauth/callback"
f"&code_challenge={code_challenge}"
f"&code_challenge_method=S256"
)[cast]
Any deep link parameter that feeds into a WebView must be validated against a strict allowlist of schemes and domains before loading.
java
String targetUrl = intent.getData().getQueryParameter("url");
if (targetUrl != null && (targetUrl.startsWith("https://targetapp.com/")
|| targetUrl.startsWith("https://cdn.targetapp.com/"))) {
webView.loadUrl(targetUrl);
} else {
// Reject and log the attempt
logSuspiciousDeepLink(targetUrl);
}On both platforms, additional context about the calling application can help distinguish expected navigation flows from suspicious ones, though this should be treated as a defense-in-depth measure rather than a sole control, since calling package information can be spoofed in some scenarios.
Deep link handling logic tends to grow organically as new features are added, and validation gaps often creep in during this expansion rather than being present from the start. Treating deep link parameters with the same suspicion as any other untrusted user input, reviewing intent filter and URL scheme changes during code review, and including deep link fuzzing in the mobile QA and security testing pipeline all help catch these issues before release rather than after a public disclosure.
Organizations shipping mobile applications with sensitive deep link driven flows, particularly authentication, payments, or account recovery, benefit from a dedicated mobile security assessment that specifically targets this attack surface rather than relying on general application testing alone. Sagefox Cybersecurity includes deep link and intent handling review as a standard part of its mobile application security assessments.
Deep links are a convenient navigation mechanism that too often gets treated as trusted input rather than the untrusted, externally controllable data it actually is. Scheme hijacking, OAuth token interception, parameter injection, and WebView injection all stem from the same root issue: an app trusting the contents of a URL simply because it matches an expected format. Verified App Links and Universal Links close the scheme collision gap at the platform level, while server-side validation, PKCE, and strict WebView allowlisting close the remaining gaps at the application level.
Any app handling sensitive actions through deep links should treat this surface as a first-class part of its security testing program, not an afterthought discovered only after an incident. Teams looking to assess their mobile app's deep link handling and broader attack surface can work with Sagefox Cybersecurity to scope a mobile security assessment suited to their application's architecture.