daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

ios-custom-uri-handlers-deeplinks-custom-schemes.md (17218B)


      1 ---
      2 title: "iOS Custom URI Handlers / Deeplinks / Custom Schemes"
      3 section: "Mobile"
      4 sectionSlug: "mobile-pentesting"
      5 sourcePath: "src/mobile-pentesting/ios-pentesting/ios-custom-uri-handlers-deeplinks-custom-schemes.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/mobile-pentesting/ios-pentesting/ios-custom-uri-handlers-deeplinks-custom-schemes.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # iOS Custom URI Handlers / Deeplinks / Custom Schemes
     14 
     15 ## Basic Information
     16 
     17 Custom URL schemes enable apps to communicate using a custom protocol, as detailed in the [Apple Developer Documentation](https://developer.apple.com/library/content/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/Inter-AppCommunication/Inter-AppCommunication.html#//apple_ref/doc/uid/TP40007072-CH6-SW1).<sup>[[1]](#references)</sup> These schemes must be declared by the app, which then handles incoming URLs following those schemes. It's crucial to **validate all URL parameters** and **discard any malformed URLs** to prevent attacks through this vector.<sup>[[8]](#references)</sup>
     18 
     19 An example is given where the URI `myapp://hostname?data=123876123` invokes a specific application action. A noted vulnerability was in the Skype Mobile app, which allowed unpermitted call actions via the `skype://` protocol. The registered schemes can be found in the app's `Info.plist` under `CFBundleURLTypes`. Malicious applications can exploit this by re-registering URIs to intercept sensitive information.
     20 
     21 ### Application Query Schemes Registration
     22 
     23 From iOS 9.0, to check if an app is available, `canOpenURL:` requires declaring URL schemes in the `Info.plist` under `LSApplicationQueriesSchemes`. Apps linked on or after iOS 15 are limited to **50 entries** in this allowlist, which reduces app-enumeration abuse but is still useful to pentesters because it exposes which third-party handlers the app expects to interact with.
     24 
     25 ```xml
     26 <key>LSApplicationQueriesSchemes</key>
     27 <array>
     28     <string>url_scheme1</string>
     29     <string>url_scheme2</string>
     30 </array>
     31 ```
     32 
     33 ### Testing URL Handling and Validation
     34 
     35 Developers should inspect specific methods in the source code to understand URL path construction and validation, such as `application:didFinishLaunchingWithOptions:` and `application:openURL:options:`. For modern scene-based apps, also inspect `scene:willConnectToSession:options:` and `scene:openURLContexts:` because a lot of current iOS apps no longer route custom schemes through `UIApplicationDelegate`.
     36 
     37 For instance, Telegram employs various methods for opening URLs:
     38 
     39 ```swift
     40 func application(_ application: UIApplication, open url: URL, sourceApplication: String?) -> Bool {
     41     self.openUrl(url: url)
     42     return true
     43 }
     44 
     45 func application(_ application: UIApplication, open url: URL, sourceApplication: String?,
     46 annotation: Any) -> Bool {
     47     self.openUrl(url: url)
     48     return true
     49 }
     50 
     51 func application(_ app: UIApplication, open url: URL,
     52 options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
     53     self.openUrl(url: url)
     54     return true
     55 }
     56 
     57 func application(_ application: UIApplication, handleOpen url: URL) -> Bool {
     58     self.openUrl(url: url)
     59     return true
     60 }
     61 ```
     62 
     63 If the app uses scenes, look for code such as:
     64 
     65 ```swift
     66 func scene(_ scene: UIScene,
     67            willConnectTo session: UISceneSession,
     68            options connectionOptions: UIScene.ConnectionOptions) {
     69     if let urlContext = connectionOptions.urlContexts.first {
     70         let url = urlContext.url
     71         // parse and route the URL
     72     }
     73 }
     74 
     75 func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
     76     guard let url = URLContexts.first?.url else { return }
     77     // parse and route the URL
     78 }
     79 ```
     80 
     81 ### Modern Framework Entry Points
     82 
     83 A lot of current iOS targets are not pure UIKit anymore, so custom-scheme handling may be split across native and JS/router layers:
     84 
     85 - **SwiftUI** apps can receive deeplinks inside `.onOpenURL { url in ... }` attached to a `WindowGroup` or another `Scene`. Review every scene, not only `AppDelegate`, because a secondary scene may parse URLs differently.
     86 - **React Native / Expo** apps often forward the URL through `RCTLinkingManager`, `RCTOpenURLNotification`, `Linking.getInitialURL()`, and runtime `url` events.<sup>[[5]](#references)</sup> In Expo-managed apps, if the developer does not define an explicit custom scheme, the generated iOS bundle identifier commonly becomes a reachable default scheme. Development builds may also expose `exp://` or an extra Expo dev-client-generated scheme, so test builds often have more reachable handlers than production.<sup>[[6]](#references)</sup>
     87 - **Capacitor** apps commonly expose deeplinks through `App.getLaunchUrl()` and `App.addListener('appUrlOpen', ...)`.
     88 
     89 From a pentest perspective, the interesting question is whether the framework router later turns attacker-controlled path/query data into navigation, auth state changes, feature flags, or WebView destinations. If the same URL is later reused in a browser/WebView sink, continue with [iOS protocol handlers](/hacktricks/mobile-pentesting/ios-pentesting/ios-protocol-handlers).
     90 
     91 ### Static Triage in Compiled Apps
     92 
     93 Without source code, start from `Info.plist` and then pivot into the handlers that consume the URL. Useful checks:
     94 
     95 ```bash
     96 # Extract custom schemes and canOpenURL allowlist from an IPA/app bundle
     97 plutil -p Payload/App.app/Info.plist | rg 'CFBundleURLTypes|CFBundleURLSchemes|LSApplicationQueriesSchemes'
     98 
     99 # If you only have the IPA:
    100 unzip -p app.ipa 'Payload/*.app/Info.plist' > /tmp/Info.plist
    101 plutil -p /tmp/Info.plist | rg 'CFBundleURLTypes|CFBundleURLSchemes|LSApplicationQueriesSchemes'
    102 
    103 # Find relevant handlers and outbound opens in ObjC/Swift symbols/strings
    104 rabin2 -zzq Payload/App.app/AppBinary | rg 'openURL|canOpenURL|openURLContexts|continueUserActivity|x-success|x-error|x-cancel|RCTOpenURLNotification|appUrlOpen|getLaunchUrl|onOpenURL'
    105 
    106 # Check whether the target also has associated domains or relies only on custom schemes
    107 codesign -d --entitlements :- Payload/App.app/AppBinary 2>/dev/null | rg 'com.apple.developer.associated-domains|application-identifier'
    108 
    109 # If you have source code, include framework-specific routers and generated config as well
    110 rg -n '\.onOpenURL|OpenURLAction|RCTLinkingManager|RCTOpenURLNotification|getInitialURL|appUrlOpen|getLaunchUrl|expo\.scheme|ios\.bundleIdentifier|addGeneratedScheme|callbackURLScheme|redirectSystemPath' .
    111 ```
    112 
    113 Interesting findings during static analysis:
    114 
    115 - Custom schemes carrying **reusable secrets** such as OAuth codes, magic links, password-reset tokens, device-binding tokens, or invitation tokens.
    116 - Router code that maps attacker-controlled path/query values directly into **privileged actions** such as logout, wallet transfer, KYC steps, or account linking.
    117 - URL parameters later reused as **network targets**, `WKWebView` destinations, or file paths.
    118 - `x-success`, `x-error`, or `x-cancel` callback parameters accepted from untrusted sources and then reopened without strict validation.
    119 - Framework-generated or framework-forwarded handlers (for example bundle-ID schemes, JS bridge events, or router adapters) that developers may not realize are still part of the external attack surface.
    120 
    121 ### Source-application validation
    122 
    123 Custom schemes are an **inter-app IPC** boundary, so check whether the handler makes security decisions from the routing context (`sourceApplication`, scene options, callback state, or whether the app was cold-started) instead of from server-side proof or user re-authentication.<sup>[[7]](#references)</sup> In modern scene-based apps, the same information is reachable via `UIOpenURLContext`:
    124 
    125 ```swift
    126 func scene(_ scene: UIScene, openURLContexts contexts: Set<UIOpenURLContext>) {
    127     guard let ctx = contexts.first else { return }
    128     let url = ctx.url
    129     let sourceApp = ctx.options.sourceApplication
    130     // test whether privileged routes still work when sourceApp is nil or unexpected
    131 }
    132 ```
    133 
    134 From a pentest perspective, interesting bugs are:
    135 
    136 - Sensitive routes that execute even when `sourceApplication` is missing, unexpected, or obviously attacker-controlled.
    137 - Code paths that trust the incoming app identity more than the **contents** of the URL (for example, they skip auth checks for password-reset, wallet, or account-linking routes).
    138 - Fallback logic where the app logs an untrusted origin but still continues routing the deeplink.
    139 
    140 ### Testing URL Requests to Other Apps
    141 
    142 Methods like `openURL:options:completionHandler:` are crucial for opening URLs to interact with other apps. Identifying usage of such methods in the app's source code is key for understanding external communications.
    143 
    144 ### Testing for Deprecated Methods
    145 
    146 Deprecated methods handling URL openings, such as `application:handleOpenURL:` and `openURL:`, should be identified and reviewed for security implications.
    147 
    148 ### Triggering Custom Schemes During Dynamic Analysis
    149 
    150 Custom schemes are easy to exercise repeatedly in the simulator:
    151 
    152 ```bash
    153 xcrun simctl openurl booted 'myapp://debug?action=reset&token=AAAA'
    154 
    155 # Hybrid stacks often expose convenient test helpers as well
    156 npx uri-scheme open 'myapp://debug?action=reset&token=AAAA' --ios
    157 ```
    158 
    159 This is useful for quickly replaying payloads while observing logs, breakpoints, Frida hooks, or proxy traffic. On a real device, the same payloads can be delivered from Notes, Safari, Messages, QR codes, or a helper application that calls `UIApplication.open`. For Expo-style targets, also try the bundle identifier as the scheme if the project never defined a custom one explicitly.
    160 
    161 When instrumenting the target, hook both inbound handlers and outbound launches:
    162 
    163 - `application:openURL:options:`
    164 - `scene:openURLContexts:`
    165 - `application:continueUserActivity:restorationHandler:` when the same router also handles universal links
    166 - `+[RCTLinkingManager application:openURL:options:]` in React Native builds
    167 - `openURL:options:completionHandler:` to see which third-party apps or callbacks the target invokes
    168 
    169 For React Native / Expo targets, notification-level hooks are also useful because the JS router may consume the URL after the native handler returns:
    170 
    171 ```javascript
    172 if (ObjC.classes.RCTLinkingManager) {
    173   Interceptor.attach(ObjC.classes.RCTLinkingManager['+ application:openURL:options:'].implementation, {
    174     onEnter(args) { console.log('[RCTLinkingManager] ' + new ObjC.Object(args[3]).absoluteString()); }
    175   });
    176 }
    177 Interceptor.attach(ObjC.classes.NSNotificationCenter['- postNotificationName:object:userInfo:'].implementation, {
    178   onEnter(args) {
    179     const name = ObjC.Object(args[2]).toString();
    180     if (name.indexOf('RCTOpenURLNotification') !== -1) console.log('[NSNotification] ' + name);
    181   }
    182 });
    183 ```
    184 
    185 If the app is hybrid, compare **cold start** and **warm start** behaviour separately: `getInitialURL()` / `getLaunchUrl()` often parses the launch URL through a different code path than runtime URL events, and bugs sometimes appear in only one of them.
    186 
    187 ### Fuzzing URL Schemes
    188 
    189 Fuzzing URL schemes can identify parsing bugs and, in rare cases, memory-corruption bugs. Tools like [Frida](https://codeshare.frida.re/@dki/ios-url-scheme-fuzzing/) can automate this process by opening URLs with varying payloads to monitor for crashes, exemplified by the manipulation of URLs in the iGoat-Swift app:
    190 
    191 ```bash
    192 $ frida -U SpringBoard -l ios-url-scheme-fuzzing.js
    193 [iPhone::SpringBoard]-> fuzz("iGoat", "iGoat://?contactNumber={0}&message={0}")
    194 Watching for crashes from iGoat...
    195 No logs were moved.
    196 Opened URL: iGoat://?contactNumber=0&message=0
    197 ```
    198 
    199 If you already know which handler is used, [`furlzz`](https://github.com/NSEcho/furlzz) is useful because it fuzzes **in-process** through multiple entry points, including `application:openURL:options:`, `scene:openURLContexts:`, and universal-link handlers. This is practical when SpringBoard-driven delivery is too noisy and you want to focus on the target parser itself.
    200 
    201 Useful payload classes:
    202 
    203 - Overlong path segments and query values.
    204 - Mixed encodings (`%00`, double-encoded `%252f`, invalid UTF-8).
    205 - Duplicate keys (`id=1&id=2`) to catch inconsistent parsing between router and business logic.
    206 - Unexpected callback values such as `x-success=otherapp://cb` or nested `redirect=` parameters.
    207 
    208 ## x-callback-url style abuse
    209 
    210 Many iOS apps implement [x-callback-url](https://x-callback-url.com/specification/) semantics on top of custom schemes, commonly exposing `x-success`, `x-error`, and `x-cancel` parameters.<sup>[[3]](#references)</sup> From a pentest perspective, this creates two recurring attack surfaces:
    211 
    212 - **Callback redirection / app bouncing**: If the target app accepts an arbitrary callback URL and later re-opens it, you can chain execution into another app or into a malicious scheme you control.
    213 - **Data exfiltration through callbacks**: If sensitive results are appended to `x-success` (IDs, auth artifacts, search results, file locations, prefilled content, etc.), a rogue app can register the callback scheme and harvest them.
    214 
    215 Testers should verify whether the app:
    216 
    217 - Restricts callbacks to a strict allowlist of schemes/hosts.
    218 - Avoids placing secrets or bearer-like tokens in callback URLs.
    219 - Requires user interaction before performing destructive or externally visible actions triggered from a URL.
    220 
    221 ## Custom URL scheme hijacking
    222 
    223 Apple explicitly notes that **if multiple apps register the same scheme, the app chosen by the system is undefined**.<sup>[[1]](#references)</sup> Therefore, any security-sensitive flow that returns data via a custom scheme must be treated as hijackable.
    224 
    225 According to [**this post**](https://evanconnelly.com/post/ios-oauth/), a malicious app can **register another app's custom scheme** and then abuse [ASWebAuthenticationSession](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession/2990952-init#parameters) to run OAuth in a browser context that still has Safari cookies.<sup>[[2]](#references)</sup>
    226 
    227 The attack flow is:
    228 
    229 1. The malicious app starts an `ASWebAuthenticationSession` and sets the victim's custom scheme as the callback scheme.
    230 2. The session loads an attacker-controlled page that the user is willing to open.
    231 3. That page redirects to the victim's OAuth authorization endpoint, often with `prompt=none` to avoid user interaction.
    232 4. If the victim is already authenticated, the authorization server redirects with an authorization code (or another secret) to the victim's custom scheme.
    233 5. Because the attacker's app also registered that scheme and owns the active `ASWebAuthenticationSession`, the attacker receives the callback and can exchange the code.
    234 
    235 Practical triage shortcuts for this family of bugs:
    236 
    237 - Search for redirect URIs such as `com.example.app:/oauth2redirect/...`, `myapp://oauth/callback`, or `bundle.id://callback` and verify whether the app validates only the scheme or also enforces the expected host/path.
    238 - Grep for `state`, `nonce`, `code_verifier`, `callbackURLScheme`, `redirect_uri`, `redirectSystemPath`, `magic`, and `invite` to find non-obvious flows that reuse the same custom scheme transport.
    239 - Try the same interception idea against password-reset, email-verification, invite, and magic-link flows; these are often easier to exploit than full OAuth because the app treats the deeplink itself as sufficient proof.
    240 
    241 This is important because **PKCE alone does not save the flow** when the attacker originates the entire OAuth request and chooses its own `code_challenge` / `code_verifier`.<sup>[[2]](#references)</sup> RFC 8252 still allows private-use URI schemes for native apps, but explicitly calls out that multiple apps can register the same scheme and recommends app-claimed `https` redirects where the platform supports them.<sup>[[4]](#references)</sup> In practice, that means custom schemes are still common, but they should be treated as an attacker-reachable IPC boundary and not as proof of app identity. See also [this other page about Universal Links](/hacktricks/mobile-pentesting/ios-pentesting/ios-universal-links).
    242 
    243 ## References
    244 
    245 - [1] [Defining a custom URL scheme for your app - Apple Developer Documentation](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app)
    246 - [2] [Mobile OAuth Attacks - iOS URL Scheme Hijacking Revamped](https://evanconnelly.com/post/ios-oauth/)
    247 - [3] [x-callback-url Specification](https://x-callback-url.com/specification/)
    248 - [4] [RFC 8252 - OAuth 2.0 for Native Apps](https://www.rfc-editor.org/rfc/rfc8252)
    249 - [5] [Linking - React Native Documentation](https://reactnative.dev/docs/linking.html)
    250 - [6] [Linking into your app - Expo Documentation](https://docs.expo.dev/linking/into-your-app/)
    251 - [7] [MASTG-TEST-0371: Missing Source Validation in Custom URL Scheme Handlers](https://mas.owasp.org/MASTG/tests/ios/MASVS-PLATFORM/MASTG-TEST-0371/)
    252 - [8] [Apple Developer Documentation](https://developer.apple.com/library/content/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/Inter-AppCommunication/Inter-AppCommunication.html#//apple_ref/doc/uid/TP40007072-CH6-SW1)