iOS Deep Link Hijacking: Turning Universal Links into Account Takeover

iOS Deep Link Hijacking: Turning Universal Links into Account Takeover

Universal Links closed the classic custom-scheme hijack, but validation gaps on the receiving end still turn deep links into a real attack surface.

HackerSavanna Security Team

Security research and platform engineering at HackerSavanna.

5 min read3 views

Universal Links were supposed to be the safe replacement for custom URL schemes. Apple designed them specifically to close the hijacking risk of myapp:// style links, where any app on the device could register the same scheme and race to intercept it. In practice, Universal Links introduce their own, subtler failure modes, and they show up in mobile bug bounty reports more often than the feature's reputation for safety would suggest.

The two link types, and why they behave differently

Custom URL schemes (myapp://reset-password?token=abc) are claimed by whichever app registers that scheme first on the device. If two apps both declare myapp://, the OS has to pick one, and an attacker who gets their app installed first (or convinces the user to install it) can silently capture links meant for the legitimate app, tokens included.

Universal Links (https://target.example.com/reset-password?token=abc) fix that specific race by tying the link to a domain the app owner controls. iOS fetches a signed apple-app-site-association (AASA) file from that domain and only routes matching links to the app if the AASA explicitly authorizes it. That's real cryptographic-adjacent binding, not just a claimed string, which is why Universal Links are the recommended approach today.

The vulnerabilities that remain live in the gap between "the link is authenticated to a domain" and "the app correctly validates what's inside the link" once it receives it.

Where it actually breaks

1. Overly broad AASA path matching. A common misconfiguration:

json143 Bytes
1{
2 "applinks": {
3 "apps": [],
4 "details": [
5 {
6 "appID": "TEAMID.com.target.app",
7 "paths": ["*"]
8 }
9 ]
10 }
11}

"paths": ["*"] routes every single path under the domain into the app, including ones the developer never intended to be deep-link entry points, such as password reset confirmations, payment callbacks, or internal admin routes that happen to share the domain. If any of those paths accept a parameter that triggers a sensitive action without additional verification, the broad AASA turns a normal web page into an app-side attack surface.

2. Missing origin or state validation on the receiving side. This is the more common and more serious pattern. The app correctly receives the Universal Link, but the handler trusts every parameter in it unconditionally:

swift517 Bytes
1func application(_ application: UIApplication,
2 continue userActivity: NSUserActivity,
3 restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
4 guard let url = userActivity.webpageURL else { return false }
5 if url.path.contains("/link-account") {
6 let params = parseQueryParams(url)
7 // No check that this session actually initiated the linking flow
8 linkAccount(token: params["token"], userId: params["uid"])
9 }
10 return true
11}

If linkAccount trusts uid and token straight from the URL without confirming they correspond to a flow the current logged-in session actually started, an attacker can craft a Universal Link that links their own account, or someone else's, to an arbitrary session. Sent via SMS, embedded in a QR code, or dropped in a support chat, the victim just has to tap it while the app is installed.

3. Fallback to custom scheme. Many apps still register a custom URL scheme alongside Universal Links, either for backward compatibility or because a third-party SDK requires it. If the fallback scheme handles the same sensitive parameters with weaker validation than the Universal Link path, an attacker downgrades the attack by simply using the custom scheme instead, bypassing the AASA protection entirely.

A realistic account takeover chain

Here's the shape of a finding that typically lands Critical:

  1. The app exposes a Universal Link for OAuth-style account linking: https://target.example.com/oauth/callback?state=...&token=....
  2. The AASA authorizes the entire /oauth/* path.
  3. The receiving handler in the app extracts token and immediately calls an internal "complete linking" endpoint, without verifying that state matches a value generated by the current device's own initiated flow.
  4. An attacker starts their own linking flow to capture a valid token and state pair tied to their attacker-controlled account, then sends the resulting Universal Link to a victim.
  5. The victim, already logged into the legitimate app, taps the link. The app processes it as if the victim had initiated the linking themselves, and the victim's session ends up bound to the attacker's account, or vice versa depending on the exact flow.

The proof of concept for this kind of finding is just the crafted link plus a screen recording showing the state change on a second, victim-role device or account, which is usually enough for a triager to confirm severity without needing your source access.

What good validation looks like

  • Bind any sensitive deep link parameter (token, state, linking ID) to the session that generated it, and verify that binding server-side before acting on it, not just client-side.
  • Scope the AASA paths array as narrowly as possible. List exact routes the app needs to handle instead of wildcarding the whole domain.
  • Treat every value arriving via a Universal Link as attacker-controlled input, the same way you'd treat a query parameter on a public API endpoint, because functionally that's exactly what it is.
  • If a custom URL scheme fallback exists for legacy reasons, apply the same validation to it that the Universal Link path gets. A downgrade path is only safe if it's equally strict.

Universal Links solved the domain-hijacking problem Apple designed them to solve. They were never a substitute for validating what's inside the link once it arrives, and that gap is where the real findings live.

Share: