Auditing mobile deep-link trust: when an attacker's URL drives a trusted action
A deep link is attacker-reachable input: a custom scheme any app can claim, or a link another app or a web
page can fire, carrying parameters the app routes into an action. The bug is trusting that URL, letting a
parameter reach a sensitive operation, or handing an attacker-controlled URL to a WebView that exposes a
JavaScript bridge back into the app. This audit reads the deep-link routing, the URL handling, and the
WebView configuration and asks, per link, whether an attacker-supplied URL can drive something it should
not. It pairs with the component-exposure skill: that one owns whether the receiving component is reachable
across the app boundary, this one owns whether the URL it carries is trusted. Keep the seam clean so a
single flaw is reported once.
When to use
- The app registers a custom scheme, an app link, or a universal link, and routes the URL to an action.
- A deep-link parameter flows into navigation, a WebView load, an authentication step, or a state change.
- You want to know whether an attacker-supplied URL can drive a sensitive action or reach a trusted WebView.
Scope check
Audit only apps you own or are authorized to assess, and fire a deep link only at a device or emulator in
scope, a crafted link drives real app state and can complete real actions. Adjudicate on the routing and
the WebView config. If you can't name the authorization, stop.
The loop
Establish which links are attacker-controllable first. Determine how each link is registered: a
custom scheme (any app can also register it and intercept or forge it), an app or universal link with a
verified domain association (only the app resolves it), or one whose association is unverified (not
exclusive). The registration decides whether an attacker can send or hijack the link; settle it before
judging the handler.
Check scheme hijack and association. Look for a custom scheme carrying sensitive data or actions,
which a malicious app can register to intercept or forge, and for an app link whose domain association is
missing or unverified, so the link is not exclusively the app's and can be claimed or spoofed.
Check parameter flow into sensitive actions. Follow the deep-link parameters into the app and look
for one that reaches a sensitive sink without validation: a navigation target, an authentication or
token parameter, an account or object identifier, a redirect, or a file path. A link parameter trusted
as if it were internal is the core flaw.
Check the URL into a WebView. Look for a deep-link or app-supplied URL loaded into a WebView without
an allowlist of trusted origins, letting an attacker point the trusted WebView at content they control.
This is the WebView URL seam the component-exposure skill hands here.
Check the JavaScript bridge. Where a WebView exposes a native bridge (a JavaScript interface or a
message handler) to page content, look for that WebView being able to load untrusted or attacker-chosen
content, so a hostile page calls native methods through the bridge. An exposed bridge on a WebView that
loads only pinned first-party content is far weaker than one reachable by an attacker URL.
Confirm and record. Confirm by showing an attacker-supplied URL drives the sensitive action or
reaches the bridge. Kill the lead if the link is an app or universal link with a verified domain
association so an attacker cannot forge it, if the parameter is validated or canonicalized before the
sink, if the WebView loads only a pinned first-party origin with no attacker-controllable URL, if the
bridge is exposed only to content the app fully controls, or if the routed action is not sensitive and
changes no state. Record the link, its registration, the parameter path, and the action or bridge reached.
Where deep-link trust leaks
- A custom scheme is not exclusive. Any app can register the same scheme and intercept or forge the
link; a scheme carrying sensitive data or actions cannot be trusted as the app's alone.
- An unverified app link is claimable. Without a verified domain association the link is not
exclusively the app's; verify the association before trusting the link's origin.
- A link parameter is attacker input. A target, token, identifier, or path from a deep link is
externally supplied; trusting it as internal is the flaw, wherever it lands.
- A WebView URL from a link is an open redirect into a trusted context. Loading an attacker-chosen URL
into the app's WebView hands the attacker the WebView's trust; allowlist the origin.
- A bridge is only as safe as what the WebView loads. A native bridge exposed to a WebView that can
load untrusted content lets a hostile page call native code; the load allowlist is the control.
Worked example (a confirm and a kill)
Confirm. A WebView exposes a native bridge that reads app storage, and the URL it loads comes from a
deep-link parameter with no origin allowlist; an attacker sends a link that points the WebView at their
page, which calls the bridge and exfiltrates stored data. Confirmed an attacker-controlled URL into a
bridge-exposing WebView, critical, remediation = allowlist the WebView origin to pinned first-party
content and remove or gate the bridge.
Kill. A deep link carries a next-screen parameter, but the router resolves it against a fixed
allowlist of in-app destinations and rejects anything else, and no WebView or bridge is involved. An
attacker's value is discarded. Killed, kill_reason = "the deep-link parameter is validated against a
fixed in-app destination allowlist before use, so an attacker-supplied value cannot drive navigation."
Rationalizations to reject
- "It uses our custom scheme." -> Any app can register that scheme and intercept or forge the link; a
custom scheme is not proof the link came from a trusted source.
- "It is an app link, so it is verified." -> Is the domain association actually verified? An unverified
association makes the link claimable, not exclusive.
- "The parameter just picks a screen." -> Does it reach navigation, auth, or a path without validation? An
unvalidated link parameter is attacker input at the sink.
- "The WebView is part of our app." -> Does it load a URL an attacker can choose? A trusted WebView
pointed at hostile content lends that content the app's trust.
- "The bridge is only for our pages." -> Can the WebView be made to load an attacker page? A bridge is
only safe if the WebView cannot load untrusted content.
Executing this in practice
You need the registered schemes and app or universal links and their domain associations, the deep-link
routing and where each parameter flows, the WebView configuration and the URLs it loads, and any native
bridge exposed to a WebView. For each link, decide whether an attacker-supplied URL can drive a sensitive
action or reach the bridge. Reading the registration tells you whether an attacker can send the link;
reading the handler and the WebView config tells you what the URL can drive.
Related
auditing-android-component-exposure - the component-reach seam: that skill owns whether the component
receiving the link is exported and reachable, this one owns whether the URL it carries is trusted.
testing-client-side-dom-vulnerabilities - the web-page analogue of the WebView content this skill loads;
the same untrusted-URL-into-a-trusted-context shape in a browser.
hunting-mobile-secret-and-storage-exposure - the sibling for data at rest that a bridge or a routed
action might read; distinct from the URL-trust decision here.
- FINDING-SCHEMA.md - source = the attacker-supplied URL, sink = a sensitive action
or a trusted WebView bridge acting on it, evidence = the registration, the parameter path, and the sink reached.
1---2name: auditing-mobile-deeplink-trust3description: Audit how a mobile app handles a deep link, app link, or custom-scheme URL, so an attacker-supplied URL cannot drive a sensitive action or reach a trusted WebView context. Covers a custom scheme any app can register and hijack, an app link whose domain association is unverified so the link is not exclusively the app's, a deep-link parameter that flows unvalidated into a sensitive action, an attacker-controlled URL loaded into a WebView, and a JavaScript bridge exposed to a WebView that can load untrusted content. Use when reviewing deep-link routing, URL handling, and WebView configuration, not the manifest export state of the component that receives the link (that is the component-exposure skill). The attacker-supplied URL is the source, a sensitive action or a trusted WebView bridge acting on it is the sink, and a link parameter trusted without validation is the bug.4license: MIT5---67# Auditing mobile deep-link trust: when an attacker's URL drives a trusted action89A deep link is attacker-reachable input: a custom scheme any app can claim, or a link another app or a web10page can fire, carrying parameters the app routes into an action. The bug is trusting that URL, letting a11parameter reach a sensitive operation, or handing an attacker-controlled URL to a WebView that exposes a12JavaScript bridge back into the app. This audit reads the deep-link routing, the URL handling, and the13WebView configuration and asks, per link, whether an attacker-supplied URL can drive something it should14not. It pairs with the component-exposure skill: that one owns whether the receiving component is reachable15across the app boundary, this one owns whether the URL it carries is trusted. Keep the seam clean so a16single flaw is reported once.1718## When to use1920- The app registers a custom scheme, an app link, or a universal link, and routes the URL to an action.21- A deep-link parameter flows into navigation, a WebView load, an authentication step, or a state change.22- You want to know whether an attacker-supplied URL can drive a sensitive action or reach a trusted WebView.2324## Scope check2526Audit only apps you own or are authorized to assess, and fire a deep link only at a device or emulator in27scope, a crafted link drives real app state and can complete real actions. Adjudicate on the routing and28the WebView config. If you can't name the authorization, stop.2930## The loop31321. **Establish which links are attacker-controllable first.** Determine how each link is registered: a33 custom scheme (any app can also register it and intercept or forge it), an app or universal link with a34 verified domain association (only the app resolves it), or one whose association is unverified (not35 exclusive). The registration decides whether an attacker can send or hijack the link; settle it before36 judging the handler.37382. **Check scheme hijack and association.** Look for a custom scheme carrying sensitive data or actions,39 which a malicious app can register to intercept or forge, and for an app link whose domain association is40 missing or unverified, so the link is not exclusively the app's and can be claimed or spoofed.41423. **Check parameter flow into sensitive actions.** Follow the deep-link parameters into the app and look43 for one that reaches a sensitive sink without validation: a navigation target, an authentication or44 token parameter, an account or object identifier, a redirect, or a file path. A link parameter trusted45 as if it were internal is the core flaw.46474. **Check the URL into a WebView.** Look for a deep-link or app-supplied URL loaded into a WebView without48 an allowlist of trusted origins, letting an attacker point the trusted WebView at content they control.49 This is the WebView URL seam the component-exposure skill hands here.50515. **Check the JavaScript bridge.** Where a WebView exposes a native bridge (a JavaScript interface or a52 message handler) to page content, look for that WebView being able to load untrusted or attacker-chosen53 content, so a hostile page calls native methods through the bridge. An exposed bridge on a WebView that54 loads only pinned first-party content is far weaker than one reachable by an attacker URL.55566. **Confirm and record.** Confirm by showing an attacker-supplied URL drives the sensitive action or57 reaches the bridge. Kill the lead if the link is an app or universal link with a verified domain58 association so an attacker cannot forge it, if the parameter is validated or canonicalized before the59 sink, if the WebView loads only a pinned first-party origin with no attacker-controllable URL, if the60 bridge is exposed only to content the app fully controls, or if the routed action is not sensitive and61 changes no state. Record the link, its registration, the parameter path, and the action or bridge reached.6263## Where deep-link trust leaks6465- **A custom scheme is not exclusive.** Any app can register the same scheme and intercept or forge the66 link; a scheme carrying sensitive data or actions cannot be trusted as the app's alone.67- **An unverified app link is claimable.** Without a verified domain association the link is not68 exclusively the app's; verify the association before trusting the link's origin.69- **A link parameter is attacker input.** A target, token, identifier, or path from a deep link is70 externally supplied; trusting it as internal is the flaw, wherever it lands.71- **A WebView URL from a link is an open redirect into a trusted context.** Loading an attacker-chosen URL72 into the app's WebView hands the attacker the WebView's trust; allowlist the origin.73- **A bridge is only as safe as what the WebView loads.** A native bridge exposed to a WebView that can74 load untrusted content lets a hostile page call native code; the load allowlist is the control.7576## Worked example (a confirm and a kill)7778> **Confirm.** A WebView exposes a native bridge that reads app storage, and the URL it loads comes from a79> deep-link parameter with no origin allowlist; an attacker sends a link that points the WebView at their80> page, which calls the bridge and exfiltrates stored data. **Confirmed** an attacker-controlled URL into a81> bridge-exposing WebView, `critical`, remediation = allowlist the WebView origin to pinned first-party82> content and remove or gate the bridge.83>84> **Kill.** A deep link carries a next-screen parameter, but the router resolves it against a fixed85> allowlist of in-app destinations and rejects anything else, and no WebView or bridge is involved. An86> attacker's value is discarded. **Killed**, `kill_reason` = "the deep-link parameter is validated against a87> fixed in-app destination allowlist before use, so an attacker-supplied value cannot drive navigation."8889## Rationalizations to reject9091- *"It uses our custom scheme."* -> Any app can register that scheme and intercept or forge the link; a92 custom scheme is not proof the link came from a trusted source.93- *"It is an app link, so it is verified."* -> Is the domain association actually verified? An unverified94 association makes the link claimable, not exclusive.95- *"The parameter just picks a screen."* -> Does it reach navigation, auth, or a path without validation? An96 unvalidated link parameter is attacker input at the sink.97- *"The WebView is part of our app."* -> Does it load a URL an attacker can choose? A trusted WebView98 pointed at hostile content lends that content the app's trust.99- *"The bridge is only for our pages."* -> Can the WebView be made to load an attacker page? A bridge is100 only safe if the WebView cannot load untrusted content.101102## Executing this in practice103104You need the registered schemes and app or universal links and their domain associations, the deep-link105routing and where each parameter flows, the WebView configuration and the URLs it loads, and any native106bridge exposed to a WebView. For each link, decide whether an attacker-supplied URL can drive a sensitive107action or reach the bridge. Reading the registration tells you whether an attacker can send the link;108reading the handler and the WebView config tells you what the URL can drive.109110## Related111112- `auditing-android-component-exposure` - the component-reach seam: that skill owns whether the component113 receiving the link is exported and reachable, this one owns whether the URL it carries is trusted.114- `testing-client-side-dom-vulnerabilities` - the web-page analogue of the WebView content this skill loads;115 the same untrusted-URL-into-a-trusted-context shape in a browser.116- `hunting-mobile-secret-and-storage-exposure` - the sibling for data at rest that a bridge or a routed117 action might read; distinct from the URL-trust decision here.118- [FINDING-SCHEMA.md](../../FINDING-SCHEMA.md) - source = the attacker-supplied URL, sink = a sensitive action119 or a trusted WebView bridge acting on it, evidence = the registration, the parameter path, and the sink reached.