Skip to content

Commit 3dd968c

Browse files
authored
Merge pull request #2464 from HackTricks-wiki/research_update_src_mobile-pentesting_ios-pentesting_ios-custom-uri-handlers-deeplinks-custom-schemes_20260704_033922
Research Update Enhanced src/mobile-pentesting/ios-pentestin...
2 parents 8d506e1 + d611627 commit 3dd968c

1 file changed

Lines changed: 49 additions & 3 deletions

File tree

src/mobile-pentesting/ios-pentesting/ios-custom-uri-handlers-deeplinks-custom-schemes.md

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>)
7373
A lot of current iOS targets are not pure UIKit anymore, so custom-scheme handling may be split across native and JS/router layers:
7474

7575
- **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.
76-
- **React Native / Expo** apps often forward the URL through `RCTLinkingManager`, `RCTOpenURLNotification`, `Linking.getInitialURL()`, and runtime `url` events. 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.
76+
- **React Native / Expo** apps often forward the URL through `RCTLinkingManager`, `RCTOpenURLNotification`, `Linking.getInitialURL()`, and runtime `url` events. 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.
7777
- **Capacitor** apps commonly expose deeplinks through `App.getLaunchUrl()` and `App.addListener('appUrlOpen', ...)`.
7878

7979
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](ios-protocol-handlers.md).
@@ -93,8 +93,11 @@ plutil -p /tmp/Info.plist | rg 'CFBundleURLTypes|CFBundleURLSchemes|LSApplicatio
9393
# Find relevant handlers and outbound opens in ObjC/Swift symbols/strings
9494
rabin2 -zzq Payload/App.app/AppBinary | rg 'openURL|canOpenURL|openURLContexts|continueUserActivity|x-success|x-error|x-cancel|RCTOpenURLNotification|appUrlOpen|getLaunchUrl|onOpenURL'
9595

96-
# If you have source code, include framework-specific routers as well
97-
rg -n '\.onOpenURL|OpenURLAction|RCTLinkingManager|RCTOpenURLNotification|getInitialURL|appUrlOpen|getLaunchUrl' .
96+
# Check whether the target also has associated domains or relies only on custom schemes
97+
codesign -d --entitlements :- Payload/App.app/AppBinary 2>/dev/null | rg 'com.apple.developer.associated-domains|application-identifier'
98+
99+
# If you have source code, include framework-specific routers and generated config as well
100+
rg -n '\.onOpenURL|OpenURLAction|RCTLinkingManager|RCTOpenURLNotification|getInitialURL|appUrlOpen|getLaunchUrl|expo\.scheme|ios\.bundleIdentifier|addGeneratedScheme|callbackURLScheme|redirectSystemPath' .
98101
```
99102

100103
Interesting findings during static analysis:
@@ -105,6 +108,25 @@ Interesting findings during static analysis:
105108
- `x-success`, `x-error`, or `x-cancel` callback parameters accepted from untrusted sources and then reopened without strict validation.
106109
- 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.
107110

111+
### Source-application validation
112+
113+
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. In modern scene-based apps, the same information is reachable via `UIOpenURLContext`:
114+
115+
```swift
116+
func scene(_ scene: UIScene, openURLContexts contexts: Set<UIOpenURLContext>) {
117+
guard let ctx = contexts.first else { return }
118+
let url = ctx.url
119+
let sourceApp = ctx.options.sourceApplication
120+
// test whether privileged routes still work when sourceApp is nil or unexpected
121+
}
122+
```
123+
124+
From a pentest perspective, interesting bugs are:
125+
126+
- Sensitive routes that execute even when `sourceApplication` is missing, unexpected, or obviously attacker-controlled.
127+
- 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).
128+
- Fallback logic where the app logs an untrusted origin but still continues routing the deeplink.
129+
108130
### Testing URL Requests to Other Apps
109131

110132
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.
@@ -134,6 +156,22 @@ When instrumenting the target, hook both inbound handlers and outbound launches:
134156
- `+[RCTLinkingManager application:openURL:options:]` in React Native builds
135157
- `openURL:options:completionHandler:` to see which third-party apps or callbacks the target invokes
136158

159+
For React Native / Expo targets, notification-level hooks are also useful because the JS router may consume the URL after the native handler returns:
160+
161+
```javascript
162+
if (ObjC.classes.RCTLinkingManager) {
163+
Interceptor.attach(ObjC.classes.RCTLinkingManager['+ application:openURL:options:'].implementation, {
164+
onEnter(args) { console.log('[RCTLinkingManager] ' + new ObjC.Object(args[3]).absoluteString()); }
165+
});
166+
}
167+
Interceptor.attach(ObjC.classes.NSNotificationCenter['- postNotificationName:object:userInfo:'].implementation, {
168+
onEnter(args) {
169+
const name = ObjC.Object(args[2]).toString();
170+
if (name.indexOf('RCTOpenURLNotification') !== -1) console.log('[NSNotification] ' + name);
171+
}
172+
});
173+
```
174+
137175
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.
138176

139177
### Fuzzing URL Schemes
@@ -184,6 +222,12 @@ The attack flow is:
184222
4. If the victim is already authenticated, the authorization server redirects with an authorization code (or another secret) to the victim's custom scheme.
185223
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.
186224

225+
Practical triage shortcuts for this family of bugs:
226+
227+
- 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.
228+
- 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.
229+
- 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.
230+
187231
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`. 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. 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](ios-universal-links.md).
188232

189233
## References
@@ -193,6 +237,8 @@ This is important because **PKCE alone does not save the flow** when the attacke
193237
- [https://x-callback-url.com/specification/](https://x-callback-url.com/specification/)
194238
- [https://www.rfc-editor.org/rfc/rfc8252](https://www.rfc-editor.org/rfc/rfc8252)
195239
- [https://reactnative.dev/docs/linking.html](https://reactnative.dev/docs/linking.html)
240+
- [https://docs.expo.dev/linking/into-your-app/](https://docs.expo.dev/linking/into-your-app/)
241+
- [https://mas.owasp.org/MASTG/tests/ios/MASVS-PLATFORM/MASTG-TEST-0371/](https://mas.owasp.org/MASTG/tests/ios/MASVS-PLATFORM/MASTG-TEST-0371/)
196242

197243
{{#include ../../banners/hacktricks-training.md}}
198244

0 commit comments

Comments
 (0)