Deep Link and App Link Hijacking: How a Malicious App Steals Your Session

Deep Link and App Link Hijacking: How a Malicious App Steals Your Session

In a mobile application, a link does not always open a web page. It may launch a specific screen, complete an authentication flow, accept an invitation, recover an account, or load content inside a WebView. This capability is known as deep linking, and it is one of the most common ways to connect browsers, email, other applications, and the operating system with an installed app.

It is also a security boundary. Custom URI schemes such as myapp://callback do not prove who owns the name. Another installed application can declare the same scheme and compete to receive the link. If that link carries an OAuth response, a temporary token, or a sensitive action, the impact can go far beyond opening the wrong screen.

Android App Links and iOS Universal Links solve much of this problem by binding an application to a domain controlled by its owner. However, domain verification does not validate incoming parameters, fix weak authorization, or make a dangerous action safe. The link may reach the correct application and still exploit its internal logic.

This article explains how deep link hijacking works, what changes between Android and iOS, why OAuth is one of the most sensitive scenarios, how verified links should be audited, and which controls belong at each layer.

Note: the examples are provided for educational and defensive purposes. Testing must be limited to applications you own or are expressly authorized to assess.

In this guide


Why a deep link is a security boundary

A deep link is a URI that takes the user to a specific context inside an application. It may contain a path, query parameters, and a fragment, just like a web URL:

Custom scheme
sixhack://course/wxe?section=oauth

Verified web link
https://sixhackacademy.example/course/wxe?section=oauth

From the application's point of view, both may eventually call an internal function that decides which screen to open and which data to use. The difference is how the operating system determines which application should receive the link.

A common mistake is treating a deep link as trusted internal navigation. The link may originate from a browser, email, QR code, another app, or a malicious application installed on the device. Its parameters must therefore be treated as external input even when the domain has been verified.

Opening the app also does not establish the identity of the sender. A deep link is not a session, a signature, or an authorization decision. It is only a transport and routing mechanism.

Receiving a link must never be used, by itself, as proof of identity or permission to execute a sensitive action.

Three link models with different guarantees

TypeExampleOwner associationMain risk
Custom URI schememyapp://profileNo cryptographic or domain verification.Another app can declare the same scheme.
Unverified web deep linkhttps://app.example/profileThe app claims it can handle the URL, but the association may not be verified.Disambiguation, browser fallback, or handling by another app.
App Link / Universal Linkhttps://app.example/profileThe domain explicitly authorizes the app through a file published over HTTPS.Misconfiguration or vulnerable handler logic.

Custom schemes remain useful for closed integrations or internal navigation, but their names are not exclusive. A reverse-domain value such as com.example.app:/oauth2redirect reduces accidental collisions, but it does not stop a hostile app from registering exactly the same value.

On iOS, Apple states that when multiple apps register the same scheme, the app selected by the system is undefined. On Android, several applications can declare compatible intent filters, and the result depends on the platform version, user preferences, and whether a verified association exists. In either case, a custom scheme does not provide a strong ownership guarantee.


How custom-scheme hijacking happens

The attack begins with a collision. The legitimate app declares that it can receive a URI, and a malicious application installed on the same device declares the exact same scheme.

# The legitimate application expects
legitapp://callback

# The malicious application registers the same scheme
legitapp://callback

The operating system now has more than one candidate. Depending on the platform and its configuration, it may display a chooser, reuse an earlier preference, or deliver the URI to one of the registered handlers. The malicious app does not need to break TLS or control the network: it positions itself at the final step, when the operating system converts a redirect into inter-app communication.

The severity depends on what the link carries. If it contains only a navigation route, the result may be limited to a poor user experience. If it includes an OAuth code, password-recovery token, privileged invitation, or an identifier that triggers an operation, the collision becomes a security issue.

There is also a less visible variant: the legitimate app receives the link correctly but processes its parameters unsafely. In that case, the problem is not handler hijacking but abuse of the entry point itself.


The critical case: intercepting an OAuth response

Native applications commonly start OAuth in an external browser. After the user authenticates, the authorization server needs to return the response to the app. A historical option is to use a custom scheme as the redirect_uri:

com.example.app:/oauth2redirect/provider

A vulnerable flow can be represented as follows:

# Conceptual authorization-code interception
Step 1  The legitimate app opens the OAuth request in the browser
Step 2  The user authenticates with the real provider
Step 3  The provider redirects to legitapp://callback?code=AUTH_CODE
Step 4  A malicious app registered for that scheme receives the URI
Step 5  The malicious app attempts to redeem the code at the token endpoint

The victim may have authenticated on the correct domain and seen an entirely legitimate login screen. The weakness appears later, in the channel used to return the code to the application.

Why PKCE changes the result

PKCE binds every authorization request to a temporary secret generated by the legitimate application instance. Before opening the browser, the app creates a random code_verifier and sends its derived value, the code_challenge, to the server. When it later receives the code, it must also present the original verifier.

# Authorization request
code_challenge = BASE64URL(SHA256(code_verifier))
code_challenge_method = S256

# Code redemption
authorization_code + code_verifier

A malicious app that intercepts only the code does not know the code_verifier. The server derives the challenge again and rejects the exchange if the values do not match. This is why PKCE with S256 is an essential control for native clients.

Another misconception should be avoided: the state parameter remains important for correlating the response and protecting the transaction, but it does not replace PKCE against authorization-code interception. Likewise, a secret embedded in a native application must not be considered confidential because it can be extracted from the binary or shared by every installation.

Which redirect URI should be used

When the platform supports them, claimed and verified HTTPS links are preferable because the domain association prevents another app from legitimately claiming that URL. If a custom scheme is used, it should be specific to the application, follow a reverse-domain format, and always be combined with PKCE.

The authorization server must register and compare redirect URIs strictly. Overly broad patterns and open redirects on approved domains can divert codes to attacker-controlled locations even when the custom scheme itself is not hijacked.

OAuth security does not depend on one control: an external browser, authorization code flow, PKCE with S256, exact redirect URI registration, and correct transaction validation work together.

Beyond OAuth: other abuse patterns

Password recovery, magic links, and invitations

Password-recovery and passwordless-login links usually contain a single-use token. If that token travels through an interceptable custom scheme, another application may receive it before the legitimate app. Verified HTTPS links, short expiration, one-time use, and server-side validation are the appropriate defenses.

Invitations to organizations or privileged spaces require the same care. Opening the link may display the invitation, but accepting it should require a valid session and confirmation bound to the correct user.

Sensitive actions triggered directly

A deep link such as myapp://transfer?to=...&amount=... should not perform a transfer merely because the URI matches a known route. A safer pattern opens a confirmation screen, retrieves current state from the server, and rechecks authentication, authorization, and business rules.

WebViews controlled by parameters

If a parameter decides which URL a WebView loads, the deep link may become arbitrary navigation:

myapp://web?url=https://attacker.example

Validating the URL with contains, startsWith, or raw string comparisons commonly creates bypasses. The app should parse the URI, require the expected scheme, and compare the normalized host against an exact allowlist. Paths, ports, redirects, and special schemes interpreted by the WebView must also be considered.

Intent redirection on Android

An exported activity may receive an intent or URI and then launch another intent that is partly or fully controlled by the attacker. This can turn a public component into a bridge toward internal features, services, or content providers that were not exported.

Android 16 introduces default hardening against certain intent-redirection attacks, but applications must still validate destinations, clear permission flags, and avoid forwarding untrusted nested intents.

Injection and path manipulation

Parameters may end up in queries, searches, templates, file paths, or object identifiers. The deep link is only the entry point; the final impact depends on the destination that consumes the data.

Parameter destinationRisk
WebView or internal browserPhishing, arbitrary navigation, XSS, or abuse of JavaScript interfaces.
File pathPath traversal, reading, or overwriting files.
Query or commandInjection when parameterization or validation is missing.
Object identifierIDOR/BOLA if ownership is not enforced.
Nested intentIndirect access to internal components or permissions.

Android App Links in depth

Android App Links use http or https URLs and establish an association between a domain and an application. The app requests verification in its manifest, and the domain publishes a Digital Asset Links file.

AndroidManifest.xml declaration

<intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />

    <data
        android:scheme="https"
        android:host="app.example"
        android:pathPrefix="/mobile/" />
</intent-filter>

The android:autoVerify="true" attribute asks the operating system to verify the association. Its presence does not prove that verification has succeeded.

assetlinks.json

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.app",
      "sha256_cert_fingerprints": ["AA:BB:CC:DD:EE:FF:..."]
    }
  }
]

It must be published at the exact location:

https://app.example/.well-known/assetlinks.json

Official documentation requires HTTPS, valid JSON, and access without redirects. Every declared host must serve its own association. When Play App Signing is used, the relevant fingerprint may be the certificate managed by the store rather than the one obtained from the local keystore.

Common failures

  • Publishing the file on www.example while the manifest declares example, or the other way around.
  • Returning a 301 or 302 before reaching assetlinks.json.
  • Using the debug fingerprint or the wrong local signing certificate.
  • Combining schemes, hosts, and paths in one filter without considering that <data> elements may be merged into additional combinations.
  • Declaring overly broad paths and routing more URLs to the mobile handler than necessary.
  • Assuming that autoVerify means the state is actually verified.

Recent Android changes

Starting with Android 12, generic web intents normally resolve to the browser unless the application has been approved for the domain or the user has configured a preference. This reduces part of the historical hijacking surface, but it does not protect custom schemes.

Android 15 adds Dynamic App Links. Rules in assetlinks.json can refine paths, parameters, fragments, and exclusions without publishing a new application version. This flexibility also makes the server-side file more operationally important: an incorrect update can expand which links are routed into the app.


iOS Universal Links in depth

Universal Links use HTTPS URLs and a two-way association between the app and the domain. The application declares the domain through the Associated Domains entitlement, and the server publishes the apple-app-site-association file, commonly called AASA.

Associated Domains entitlement

applinks:app.example

The value contains the domain only, without a path, query parameters, or trailing slash.

AASA file

{
  "applinks": {
    "details": [
      {
        "appIDs": ["TEAMID.com.example.app"],
        "components": [
          { "/": "/mobile/*" },
          { "/": "/mobile/admin/*", "exclude": true }
        ]
      }
    ]
  }
}

The extensionless file is published at:

https://app.example/.well-known/apple-app-site-association

It must be served over HTTPS with a valid certificate and without redirects. Every subdomain in use needs its corresponding entitlement and association. On modern iOS versions, the system retrieves AASA through an Apple-managed CDN, which introduces caching and means that changes may not become effective immediately.

A verified link still requires validation

Apple explicitly warns that Universal Links remain an input vector. The application should parse URLs through component APIs, reject malformed values, and limit the operations that can be triggered by a link.

A verified domain answers “which app may receive this URL?”. It does not answer “what may this URL do inside the application?”. The handler still owns that second decision.


Why verified links are not immune

App Links and Universal Links remove custom-scheme collision for the verified domain, but they do not automatically solve the rest of the threat model.

  • Manipulated parameters: an attacker can build a valid URL with unexpected values.
  • Overly broad rules: a pattern matching the whole domain sends routes to the app that were never designed for mobile handling.
  • Open redirects: a trusted path that redirects freely can support OAuth diversion, phishing, or navigation to external content.
  • Abandoned or compromised domains: the association still trusts a web property the organization may no longer control.
  • Actions without confirmation: domain verification does not authorize a transfer, deletion, or invitation acceptance.
  • Weak authorization: a link to /invoice/123 still requires proof that the invoice belongs to the user.
  • Unsafe fallback: if association fails, an implementation may fall back to a vulnerable custom scheme.
“Verified” describes routing between a domain and an app. It does not mean the logic executed afterward is secure.

Audit methodology

1. Build an inventory

On Android, inspect manifest <intent-filter> elements, exported activities, and the combinations produced by multiple <data> elements. On iOS, review CFBundleURLTypes, Associated Domains, entitlements, and the AASA contents.

Classify every entry as a custom scheme, unverified web link, verified App Link or Universal Link, OAuth redirect URI, or a link that triggers a sensitive action.

2. Locate the handler

Trace the data from the URI to the function that processes it. On Android, look for getIntent(), getData(), getQueryParameter(), and subsequent navigation. On iOS, review the delegate methods that receive custom URLs or web-browsing activities.

3. Trigger controlled links

ADB can be used on Android:

# Invoke a custom scheme
adb shell am start -W \
  -a android.intent.action.VIEW \
  -d "demoapp://profile?id=123"

# Review App Link associations on Android 12+
adb shell pm get-app-links com.example.app

# Obtain package and intent-filter information
adb shell dumpsys package com.example.app

On the iOS simulator:

# Custom scheme
xcrun simctl openurl booted "demoapp://profile?id=123"

# HTTPS link
xcrun simctl openurl booted "https://app.example/mobile/profile?id=123"

Universal Link activation should also be tested under realistic conditions because behavior depends on the installed association and previous system choices.

4. Verify the domain association

Do not stop at the manifest or entitlement. Check the remote file, exact location, certificate, redirects, hosts, fingerprints or identifiers, and the verification state observed on the device.

5. Attack the parameters

Try missing, repeated, encoded, long, Unicode, and alternative-path values that may change parser behavior. If a nested URL exists, validate schemes, hosts, ports, fragments, and redirects.

https://app.example/mobile/web?url=https://attacker.example
https://app.example/mobile/invoice?id=OTHER_USER_ID
https://app.example/mobile/action?next=https://attacker.example

6. Review OAuth as a complete chain

  • Is authorization opened in an external browser?
  • Is authorization code flow used?
  • Is PKCE mandatory and does it use S256?
  • Is the code_verifier unique to every transaction?
  • Is the redirect URI registered and compared exactly?
  • Are state and, where applicable, nonce validated?
  • Can the code be reused or redeemed from another application instance?

7. Test collisions in a controlled environment

A laboratory application can register the same scheme to demonstrate the lack of exclusive ownership. The proof of concept should be limited to showing which app receives the URI and which parameters are exposed, without accessing third-party accounts or data.

8. Evaluate real impact

Not every hijackable deep link has the same severity. The report should explain which data is intercepted, which action can be executed, what victim interaction is required, and which additional controls prevent or complete the chain.


How to defend correctly

LayerControl
RoutingUse App Links or Universal Links for sensitive external links.
OAuthAuthorization code flow, PKCE with S256, external browser, and exact redirect URI.
InputParse the URI and validate scheme, host, port, path, and parameters with allowlists.
AuthorizationRecheck identity, role, and object ownership on the backend.
Sensitive actionsRequire confirmation and obtain current state from the server.
DataDo not transport sessions, credentials, or unnecessary personal data in URLs.
PlatformAvoid intent redirection, clear flags, and minimize exported components.
OperationsMonitor domains, certificates, and association files throughout their lifecycle.

For external links, custom schemes should be the exception. When they cannot be avoided, use app-specific names, avoid carrying sensitive data, and assume that another application can register the same scheme.

Host validation must operate on parsed components rather than text searches. A check such as host.contains("example.com") may accept example.com.attacker.test. Comparison should use exact hosts or correctly bounded approved subdomains.

Password-recovery, verification, and invitation tokens should be single-use, expire quickly, and be bound to their intended purpose. Intercepting the link should not automatically grant a durable session.

Finally, domain association needs continuous testing. Changes to certificates, signing, subdomains, CDNs, or server configuration can cause a previously verified link to stop being verified without any change to the application code.


What this means for offensive security

Deep links connect several areas that are often studied separately: OAuth, web authorization, Android intents, iOS entitlements, WebViews, navigation, and domain configuration.

This makes them particularly valuable during an assessment. An analyst who only inspects the manifest may identify a collision but still needs OAuth knowledge to determine whether the intercepted code has value. Someone focused only on the backend may miss that a hostile application controls the final redirect hop. Impact emerges when both layers are analyzed together.

This is the kind of reasoning developed in the Mobile eXploitation Specialist (MXS) course, which covers Android and iOS application testing, platform interaction, WebViews, and exposed components. OAuth, PKCE, and authorization logic are complemented by the Web eXploitation Expert (WXE) course.

Want to learn how to audit mobile applications beyond surface-level analysis?

At SixHack Academy, the Mobile eXploitation Specialist (MXS) course develops a complete methodology for analyzing Android and iOS applications, including deep links, Universal Links, App Links, WebViews, and interaction between components.


Frequently asked questions

Why can a custom scheme be hijacked?

Because registration does not prove ownership. Another application can declare the same scheme. On iOS, the target is undefined when multiple apps register it; on Android, disambiguation, preferences, and platform version may affect the result.

Does stealing an authorization code always lead to account takeover?

No. The attacker must be able to redeem it. Correctly implemented PKCE binds the code to the legitimate instance's code_verifier, making an intercepted code useless by itself.

Does the state parameter replace PKCE?

No. They protect related but different parts of the transaction. Native clients should use PKCE and also validate transaction state correctly.

Do App Links and Universal Links remove every risk?

No. They prevent another app from legitimately claiming the verified domain link, but parameters remain untrusted input and the app must still enforce authentication, authorization, and validation.

Is it unsafe that assetlinks.json and AASA expose app identifiers?

No. Those files are public by design and exist to declare the association. Private signing keys must remain protected, and the organization must retain control over the domain and published configuration.

What should be tested after a signing or domain change?

Association-file availability, fingerprints or identifiers, verification state on real devices, fallback behavior, and every sensitive route that depends on the link.

Can a deep link open a sensitive action directly?

It may open the relevant screen, but the operation should still require a valid session, backend authorization, and confirmation when the impact justifies it.


References

← Back to Articles