|
2 | 2 |
|
3 | 3 | {{#include ../../banners/hacktricks-training.md}} |
4 | 4 |
|
| 5 | +## Basic Information |
5 | 6 |
|
| 7 | +In this page, **protocol handlers** are the URL schemes or URL-like handoffs that make iOS leave the current web context or resolve content through a non-standard path. During a pentest, treat every transition from **web content** to **`UIApplication.open`**, **`canOpenURL`**, or a **`WKURLSchemeHandler`** as a trust boundary. |
6 | 8 |
|
| 9 | +This page focuses on **WebView / browser-driven scheme abuse**. For app registration, deeplink hijacking, and callback stealing, see [iOS Custom URI Handlers / Deeplinks / Custom Schemes](ios-custom-uri-handlers-deeplinks-custom-schemes.md). For the file-origin / `loadFileURL:allowingReadAccessTo:` angle, see [iOS WebViews](ios-webviews.md). For claimed `https` handlers, see [iOS Universal Links](ios-universal-links.md). |
7 | 10 |
|
| 11 | +Common protocol-handler surfaces: |
8 | 12 |
|
| 13 | +- System schemes such as `tel:`, `sms:`, `mailto:`, and `facetime:`. |
| 14 | +- App schemes such as `myapp://`, browser-internal schemes, and `x-callback-url` style callbacks. |
| 15 | +- Custom resource schemes served from native code via `WKURLSchemeHandler` (for example `app://` or `resources://` inside `WKWebView`). |
| 16 | + |
| 17 | +The key question is always: **can attacker-controlled content make the app open, resolve, or bounce to a URL whose scheme/host/path was not supposed to be reachable?** |
| 18 | + |
| 19 | +## High-value bug patterns |
| 20 | + |
| 21 | +### 1. Web content controls the next navigation |
| 22 | + |
| 23 | +If a `WKWebView` renders attacker-controlled HTML or attacker-controlled data is injected into the DOM, you may get a **scheme pivot** without touching native code directly. Modern payloads do not need `<script>` tags; `meta refresh`, `onerror`, and `onload` handlers are often enough to force navigation. |
| 24 | + |
| 25 | +```html |
| 26 | +<meta http-equiv="refresh" content="0; url=myapp://debug?action=test"> |
| 27 | +<img src=x onerror="window.location='myapp://debug?action=test'"> |
| 28 | +<svg onload="window.location='myapp://debug?action=test'"></svg> |
| 29 | +``` |
| 30 | + |
| 31 | +This is especially interesting when the target WebView later forwards the navigation to `UIApplication.shared.open`, when the page is local/trusted, or when the navigation reaches a browser-internal scheme. |
| 32 | + |
| 33 | +### 2. `canOpenURL` used as if it were validation |
| 34 | + |
| 35 | +A recurring anti-pattern is: |
| 36 | + |
| 37 | +```swift |
| 38 | +if UIApplication.shared.canOpenURL(url) { |
| 39 | + UIApplication.shared.open(url) |
| 40 | +} |
| 41 | +``` |
| 42 | + |
| 43 | +`canOpenURL` only answers **whether some app can handle the scheme**. It does **not** prove that the URL is expected, safe, or owned by the right app. If the attacker controls the URL, this code still turns untrusted web input into an external-app launch. |
| 44 | + |
| 45 | +### 3. `WKNavigationDelegate` or JS bridges open arbitrary URLs |
| 46 | + |
| 47 | +Look for: |
| 48 | + |
| 49 | +- `webView(_:decidePolicyFor:decisionHandler:)` |
| 50 | +- `webView(_:createWebViewWith:for:windowFeatures:)` |
| 51 | +- `WKScriptMessageHandler` methods receiving `url`, `target`, `redirect`, `openExternal`, `browser`, `share`, or `download` |
| 52 | +- Helper methods that parse a web message and immediately call `UIApplication.shared.open` |
| 53 | + |
| 54 | +A minimal dangerous pattern is: |
| 55 | + |
| 56 | +```swift |
| 57 | +func webView(_ webView: WKWebView, decidePolicyFor action: WKNavigationAction, |
| 58 | + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { |
| 59 | + guard let url = action.request.url else { return decisionHandler(.cancel) } |
| 60 | + UIApplication.shared.open(url) |
| 61 | + decisionHandler(.cancel) |
| 62 | +} |
| 63 | +``` |
| 64 | + |
| 65 | +Safer logic should parse the URL with `URLComponents`, allow only exact schemes/hosts/paths, and explicitly deny `javascript:`, `data:`, `file:`, browser-internal schemes, and unknown custom schemes unless they are a business requirement. |
| 66 | + |
| 67 | +### 4. Nested callback parameters re-open blocked schemes |
| 68 | + |
| 69 | +Do not stop testing after a direct `myapp://` or `fido:/` launch is blocked. Recent research showed that **nested callbacks** such as `x-success`, `x-error`, and `x-cancel` can re-open a blocked scheme through an intermediate app. In practice, handler **A** may reject `fido:/` directly but still open `shortcuts://...&x-error=fido:/...` and let handler **B** perform the final launch. |
| 70 | + |
| 71 | +This is why pure **blocklists** are weak. Try handler chaining, double-encoding, and browser/helper-app schemes that accept `url=`, `x-success=`, `x-error=`, or `redirect=` parameters. Recent iOS browser fixes are a good reminder that internal non-HTTP schemes reachable from web content or from another app can bypass safety checks or spoof what the user sees. |
| 72 | + |
| 73 | +Recent technique-focused lessons from 2024-2025 research and advisories: |
| 74 | + |
| 75 | +- Web content reaching a browser's **own internal deeplink scheme** can bypass safety checks that were only designed for normal `http(s)` navigation. |
| 76 | +- Redirecting from a trusted-looking `https` page to a **non-HTTP/internal scheme** can desynchronize what the user sees from what is actually opened. |
| 77 | +- Blocking a dangerous scheme directly is not enough if an intermediate handler can reopen it through **callback parameters** such as `x-error` or `x-cancel`. |
| 78 | + |
| 79 | +### 5. `WKURLSchemeHandler` turns native code into a private web server |
| 80 | + |
| 81 | +If the app registers a custom resource scheme with `setURLSchemeHandler(_:forURLScheme:)`, every request for that scheme is served by native code. Treat it as a local attack surface: |
| 82 | + |
| 83 | +- Path traversal / `%2e%2e/` into bundle or sandbox files |
| 84 | +- Arbitrary network fetchers like `app://proxy?url=https://evil` |
| 85 | +- Secret/config exposure under predictable paths such as `app://config` |
| 86 | +- Remote pages referencing the internal scheme to reach privileged resources |
| 87 | +- Origin assumptions that break once remote and local pages can both request the same custom scheme |
| 88 | + |
| 89 | +When you see `WKURLSchemeHandler`, review the `start` / `stop` handler implementation with the same mindset you would use for an embedded HTTP server. |
| 90 | + |
| 91 | +## Static triage |
| 92 | + |
| 93 | +If you have source code: |
| 94 | + |
| 95 | +```bash |
| 96 | +rg -n 'UIApplication\.shared\.open|canOpenURL|setURLSchemeHandler|WKURLSchemeHandler|decidePolicyFor|createWebViewWith|WKScriptMessageHandler|x-success|x-error|x-cancel|redirect|openExternal' . |
| 97 | +``` |
| 98 | + |
| 99 | +If you only have the IPA / app bundle: |
| 100 | + |
| 101 | +```bash |
| 102 | +plutil -p Payload/App.app/Info.plist | rg 'CFBundleURLTypes|LSApplicationQueriesSchemes' |
| 103 | +rabin2 -zzq Payload/App.app/AppBinary | \ |
| 104 | + rg 'openURL|canOpenURL|decidePolicyForNavigationAction|createWebViewWith|WKURLSchemeHandler|setURLSchemeHandler|loadFileURL:allowingReadAccessToURL:|loadHTMLString:baseURL:|x-success|x-error|x-cancel|shortcuts://|firefox://|focus://' |
| 105 | +``` |
| 106 | + |
| 107 | +Prioritize code paths where: |
| 108 | + |
| 109 | +- A URL arrives from a WebView navigation, DOM message, query parameter, QR payload, push payload, or remote config. |
| 110 | +- The code checks only a prefix like `hasPrefix("https")` or `contains("trusted.com")`. |
| 111 | +- `canOpenURL` is immediately followed by `open`. |
| 112 | +- A local HTML page or template can be influenced by user-controlled data. |
| 113 | +- `WKURLSchemeHandler` maps request paths directly to files or backend fetches. |
| 114 | + |
| 115 | +## Dynamic analysis |
| 116 | + |
| 117 | +Useful first passes: |
| 118 | + |
| 119 | +```bash |
| 120 | +# Replay custom-scheme URLs on the simulator |
| 121 | +xcrun simctl openurl booted 'myapp://debug?action=test' |
| 122 | + |
| 123 | +# Trace common sinks |
| 124 | +frida-trace -U 'TargetApp' \ |
| 125 | + -m '*[UIApplication canOpenURL:*]' \ |
| 126 | + -m '*[UIApplication openURL:*]' \ |
| 127 | + -m '*[WKWebView *loadFileURL*]' \ |
| 128 | + -m '*[WKWebView *loadHTMLString*]' |
| 129 | +``` |
| 130 | + |
| 131 | +Minimal Frida hooks are often enough to identify which schemes really escape the WebView: |
| 132 | + |
| 133 | +```javascript |
| 134 | +Interceptor.attach(ObjC.classes.UIApplication["- canOpenURL:"].implementation, { |
| 135 | + onEnter(args) { console.log("[canOpenURL] " + new ObjC.Object(args[2]).absoluteString()); } |
| 136 | +}); |
| 137 | +Interceptor.attach(ObjC.classes.UIApplication["- openURL:options:completionHandler:"].implementation, { |
| 138 | + onEnter(args) { console.log("[open] " + new ObjC.Object(args[2]).absoluteString()); } |
| 139 | +}); |
| 140 | +``` |
| 141 | + |
| 142 | +Good payload families: |
| 143 | + |
| 144 | +- Direct launches: `tel:`, `sms:`, `mailto:`, `facetime:`, `myapp://...` |
| 145 | +- Browser/helper-app chains: `shortcuts://x-callback-url/...&x-error=myapp://...` |
| 146 | +- Nested redirects: `https://trusted.example/redirect?next=myapp://...` |
| 147 | +- HTML-injection navigations: `meta refresh`, `<img onerror>`, `<svg onload>` |
| 148 | +- Encoding tricks: mixed-case schemes, `%0a`, `%09`, double-encoded `%252f`, duplicated keys |
| 149 | + |
| 150 | +If the app exposes a `WKURLSchemeHandler`, try requesting it from attacker-controlled HTML and watch for filesystem or network side effects. |
| 151 | + |
| 152 | +## What "good" looks like |
| 153 | + |
| 154 | +A hardened implementation usually has these properties: |
| 155 | + |
| 156 | +- Top-level WebView navigations are limited to a very small allowlist, ideally exact `https` origins. |
| 157 | +- External launches are explicit exceptions (`tel`, `sms`, `mailto`, `facetime`, etc.), not the default path. |
| 158 | +- `canOpenURL` is used only as availability logic, not as a security decision. |
| 159 | +- Custom schemes are never used as bearer-token transports. |
| 160 | +- `WKURLSchemeHandler` paths are canonicalized and strictly mapped to known resources. |
| 161 | +- Unknown schemes, browser-internal schemes, and callback parameters are rejected by default. |
| 162 | + |
| 163 | +## References |
| 164 | + |
| 165 | +- [https://mas.owasp.org/MASTG/tests/ios/MASVS-PLATFORM/MASTG-TEST-0077/](https://mas.owasp.org/MASTG/tests/ios/MASVS-PLATFORM/MASTG-TEST-0077/) |
| 166 | +- [https://denniskniep.github.io/posts/13-bypass-cve-2024-9956/](https://denniskniep.github.io/posts/13-bypass-cve-2024-9956/) |
| 167 | +{{#include ../../banners/hacktricks-training.md}} |
0 commit comments