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)