// HackTricks · Mobile

iOS Universal Links

iOS Universal Links

Introduction

Universal links offer a seamless redirection experience to users by directly opening content in the app, bypassing the need for Safari redirection. These links are unique and secure, as they cannot be claimed by other apps. This is ensured by hosting a apple-app-site-association JSON file on the website’s root directory, establishing a verifiable link between the website and the app. In cases where the app is not installed, Safari will take over and direct the user to the webpage, maintaining the app’s presence.

For penetration testers, the apple-app-site-association file is of particular interest as it may reveal sensitive paths, potentially including ones related to unreleased features.

Analyzing the Associated Domains Entitlement

Developers enable Universal Links by configuring the Associated Domains in Xcode’s Capabilities tab or by inspecting the .entitlements file. Each domain is prefixed with applinks:. For example, Telegram’s configuration might appear as follows:

    <key>com.apple.developer.associated-domains</key>
    <array>
        <string>applinks:telegram.me</string>
        <string>applinks:t.me</string>
    </array>

For more comprehensive insights, refer to the archived Apple Developer Documentation.

If working with a compiled application, entitlements can be extracted as outlined in this guide.

Retrieving the Apple App Site Association File

The apple-app-site-association file should be retrieved from the server using the domains specified in the entitlements.[2] Ensure the file is accessible via HTTPS directly at https://<domain>/apple-app-site-association (or /.well-known/apple-app-site-association). Tools like the Apple App Site Association (AASA) Validator can aid in this process.

Quick enumeration from a macOS/Linux shell

# assuming you have extracted the entitlements to ent.xml
doms=$(plutil -extract com.apple.developer.associated-domains xml1 -o - ent.xml | \
       grep -oE 'applinks:[^<]+' | cut -d':' -f2)
for d in $doms; do
  echo "[+] Fetching AASA for $d";
  curl -sk "https://$d/.well-known/apple-app-site-association" | jq '.'
done

AASA Triage on Modern iOS

Since iOS 14, associated-domain metadata is commonly delivered to devices through Apple’s CDN. Therefore, when links unexpectedly open in Safari, don’t stop after checking the origin file on the target domain: also check the cached CDN copy and whether the device marked the association as usable.[1]

for d in $doms; do
  echo "=== $d ==="
  for u in \
    "https://$d/.well-known/apple-app-site-association" \
    "https://$d/apple-app-site-association" \
    "https://app-site-association.cdn-apple.com/a/v1/$d"
  do
    echo "[*] $u"
    curl -skI "$u" | sed -n '1p;/content-type/ip;/location/ip'
  done
done

# On a connected Mac or device shell, verify the actual on-device association state
swcutil dl

Useful checks during triage:

  • The origin must serve the file over HTTPS, with application/json, and without redirects.
  • The CDN response should expose the same appIDs / components (or legacy paths) that you saw on the origin.
  • swcutil dl should show the applinks association as effectively verified; if not, iOS will keep falling back to Safari even if the JSON itself looks correct.

The app must implement specific methods to handle universal links correctly. The primary method to look for is application:continueUserActivity:restorationHandler:. It’s crucial that the scheme of URLs handled is HTTP or HTTPS, as others will not be supported.

In modern targets, don’t stop at UIApplicationDelegate. Universal links may also be routed through scene-based or SwiftUI entry points such as scene(_:continue:), scene(_:willConnectTo:options:), .onOpenURL, or .onContinueUserActivity(...). Review every lifecycle entry point because some apps validate URLs in one path and blindly route them in another.

func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
    guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
          let url = userActivity.webpageURL else { return }
    route(url)
}

Validating the Data Handler Method

When a universal link opens an app, an NSUserActivity object is passed to the app with the URL. Before processing this URL, it’s essential to validate and sanitize it to prevent security risks. Here’s an example in Swift that demonstrates the process:

func application(_ application: UIApplication, continue userActivity: NSUserActivity,
                 restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    // Check for web browsing activity and valid URL
    if userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL {
        application.open(url, options: [:], completionHandler: nil)
    }

    return true
}

URLs should be carefully parsed and validated, especially if they include parameters, to guard against potential spoofing or malformed data. The NSURLComponents API is useful for this purpose, as demonstrated below:

func application(_ application: UIApplication,
                 continue userActivity: NSUserActivity,
                 restorationHandler: @escaping ([Any]?) -> Void) -> Bool {
    guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
        let incomingURL = userActivity.webpageURL,
        let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true),
        let path = components.path,
        let params = components.queryItems else {
        return false
    }

    if let albumName = params.first(where: { $0.name == "albumname" })?.value,
        let photoIndex = params.first(where: { $0.name == "index" })?.value {
        // Process the URL with album name and photo index

        return true

    } else {
        // Handle invalid or missing parameters

        return false
    }
}

Through diligent configuration and validation, developers can ensure that universal links enhance user experience while maintaining security and privacy standards.

Dynamic Testing & Runtime Tracing

Universal links are easy to mis-test:

  • Typing the URL directly into Safari’s address bar usually won’t exercise the app handoff.
  • If the app opens its own https:// URL with openURL:options:completionHandler:, the request does not re-enter the universal-link receiver as if the user had tapped it externally.

A practical workflow is to paste the URL into Notes or Messages, long-press it, and confirm whether iOS offers to open the app. While triggering the link, trace both the receiver and the next routing layer:[3]

frida-trace -U "TargetApp" -m "*[* *continueUserActivity*]" -i "*open*Url*"

This is especially useful in closed-source targets: inspect the delivered webpageURL, verify the activityType is NSUserActivityTypeBrowsingWeb, and check whether the handler later forwards the URL into a WebView, browser helper, or another internal router.

Common Vulnerabilities & Pentesting Checks

#WeaknessHow to testExploitation / Impact
1Over-broad paths / components in the AASA file (e.g. "/": "*" or wildcards such as "/a/*").• Inspect the downloaded AASA and look for *, trailing slashes, or {"?": …} rules.
• Try to request unknown resources that still match the rule (https://domain.com/a/evil?_p_dp=1).
Universal-link hijacking: a malicious iOS app that registers the same domain could claim all those links and present phishing UI. A real-world example is the May 2025 Temu.com bug-bounty report where an attacker could redirect any /a/* path to their own app.[4]
2Missing server-side validation of deep-link paths.After identifying the allowed paths, issue curl/Burp requests to non-existing resources and observe HTTP status codes. Anything other than 404 (e.g. 200/302) is suspicious.An attacker can host arbitrary content behind an allowed path and serve it via the legitimate domain, increasing the success rate of phishing or session-token theft.
3App-side URL handling without scheme/host whitelisting (CVE-2024-10474 – Mozilla Focus < 132).Look for direct openURL:/open(_:options:) calls or JavaScript bridges that forward arbitrary URLs.Internal pages can smuggle myapp:// or https:// URLs that bypass the browser’s URL-bar safety checks, leading to spoofing or unintended privileged actions.[5]
4Dangerous components ordering / ineffective exclude rules.If the AASA uses components, test excluded paths and query-parameter edge cases after every broad allow rule (for example /* placed before an exclude rule).components are evaluated in order and the first match wins, so a generic allow rule can silently defeat later deny rules and expose more routing surface than intended.[1]
5Use of wildcard sub-domains (*.example.com) in the entitlement.grep for *. in the entitlements.If any sub-domain is taken over (e.g. via an unused S3 bucket), the attacker automatically gains the Universal Link binding.

Quick Checklist

  • Extract entitlements and enumerate every applinks: entry.
  • Download AASA for each entry and audit for wildcards.
  • Verify the web server returns 404 for undefined paths.
  • In the binary, confirm that only trusted hosts/schemes are handled.
  • If the app uses the newer components syntax (iOS 11+), fuzz query-parameter rules ({"?":{…}}).
  • Verify that exclude rules appear before broad allow rules in components.
  • Compare the origin AASA with Apple’s CDN copy and inspect swcutil dl if links still open in Safari.

Tools

  • GetUniversal.link: Helps simplify the testing and management of your app’s Universal Links and AASA file. Simply enter your domain to verify AASA file integrity or use the custom dashboard to easily test link behavior. This tool also helps you determine when Apple will next index your AASA file.
  • Knil: Open-source iOS utility that fetches, parses and lets you tap-test every Universal Link declared by a domain directly on device.
  • universal-link-validator: CLI / web validator that performs strict AASA conformance checks and highlights dangerous wildcards.
  • swcutil: Built-in Apple utility that shows the actual associated-domain verification state stored on the device.
  • frida-trace: Fast way to instrument continueUserActivity handlers and downstream URL-routing code in closed-source apps.

References