Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions src/mobile-pentesting/ios-pentesting/ios-protocol-handlers.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,166 @@

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

## Basic Information

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.

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).

Common protocol-handler surfaces:

- System schemes such as `tel:`, `sms:`, `mailto:`, and `facetime:`.
- App schemes such as `myapp://`, browser-internal schemes, and `x-callback-url` style callbacks.
- Custom resource schemes served from native code via `WKURLSchemeHandler` (for example `app://` or `resources://` inside `WKWebView`).

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?**

## High-value bug patterns

### 1. Web content controls the next navigation

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.

```html
<meta http-equiv="refresh" content="0; url=myapp://debug?action=test">
<img src=x onerror="window.location='myapp://debug?action=test'">
<svg onload="window.location='myapp://debug?action=test'"></svg>
```

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.

### 2. `canOpenURL` used as if it were validation

A recurring anti-pattern is:

```swift
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
}
```

`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.

### 3. `WKNavigationDelegate` or JS bridges open arbitrary URLs

Look for:

- `webView(_:decidePolicyFor:decisionHandler:)`
- `webView(_:createWebViewWith:for:windowFeatures:)`
- `WKScriptMessageHandler` methods receiving `url`, `target`, `redirect`, `openExternal`, `browser`, `share`, or `download`
- Helper methods that parse a web message and immediately call `UIApplication.shared.open`

A minimal dangerous pattern is:

```swift
func webView(_ webView: WKWebView, decidePolicyFor action: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
guard let url = action.request.url else { return decisionHandler(.cancel) }
UIApplication.shared.open(url)
decisionHandler(.cancel)
}
```

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.

### 4. Nested callback parameters re-open blocked schemes

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.

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.

Recent technique-focused lessons from 2024-2025 research and advisories:

- Web content reaching a browser's **own internal deeplink scheme** can bypass safety checks that were only designed for normal `http(s)` navigation.
- Redirecting from a trusted-looking `https` page to a **non-HTTP/internal scheme** can desynchronize what the user sees from what is actually opened.
- 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`.

### 5. `WKURLSchemeHandler` turns native code into a private web server

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:

- Path traversal / `%2e%2e/` into bundle or sandbox files
- Arbitrary network fetchers like `app://proxy?url=https://evil`
- Secret/config exposure under predictable paths such as `app://config`
- Remote pages referencing the internal scheme to reach privileged resources
- Origin assumptions that break once remote and local pages can both request the same custom scheme

When you see `WKURLSchemeHandler`, review the `start` / `stop` handler implementation with the same mindset you would use for an embedded HTTP server.

## Static triage

If you have source code:

```bash
rg -n 'UIApplication\.shared\.open|canOpenURL|setURLSchemeHandler|WKURLSchemeHandler|decidePolicyFor|createWebViewWith|WKScriptMessageHandler|x-success|x-error|x-cancel|redirect|openExternal' .
```

If you only have the IPA / app bundle:

```bash
plutil -p Payload/App.app/Info.plist | rg 'CFBundleURLTypes|LSApplicationQueriesSchemes'
rabin2 -zzq Payload/App.app/AppBinary | \
rg 'openURL|canOpenURL|decidePolicyForNavigationAction|createWebViewWith|WKURLSchemeHandler|setURLSchemeHandler|loadFileURL:allowingReadAccessToURL:|loadHTMLString:baseURL:|x-success|x-error|x-cancel|shortcuts://|firefox://|focus://'
```

Prioritize code paths where:

- A URL arrives from a WebView navigation, DOM message, query parameter, QR payload, push payload, or remote config.
- The code checks only a prefix like `hasPrefix("https")` or `contains("trusted.com")`.
- `canOpenURL` is immediately followed by `open`.
- A local HTML page or template can be influenced by user-controlled data.
- `WKURLSchemeHandler` maps request paths directly to files or backend fetches.

## Dynamic analysis

Useful first passes:

```bash
# Replay custom-scheme URLs on the simulator
xcrun simctl openurl booted 'myapp://debug?action=test'

# Trace common sinks
frida-trace -U 'TargetApp' \
-m '*[UIApplication canOpenURL:*]' \
-m '*[UIApplication openURL:*]' \
-m '*[WKWebView *loadFileURL*]' \
-m '*[WKWebView *loadHTMLString*]'
```

Minimal Frida hooks are often enough to identify which schemes really escape the WebView:

```javascript
Interceptor.attach(ObjC.classes.UIApplication["- canOpenURL:"].implementation, {
onEnter(args) { console.log("[canOpenURL] " + new ObjC.Object(args[2]).absoluteString()); }
});
Interceptor.attach(ObjC.classes.UIApplication["- openURL:options:completionHandler:"].implementation, {
onEnter(args) { console.log("[open] " + new ObjC.Object(args[2]).absoluteString()); }
});
```

Good payload families:

- Direct launches: `tel:`, `sms:`, `mailto:`, `facetime:`, `myapp://...`
- Browser/helper-app chains: `shortcuts://x-callback-url/...&x-error=myapp://...`
- Nested redirects: `https://trusted.example/redirect?next=myapp://...`
- HTML-injection navigations: `meta refresh`, `<img onerror>`, `<svg onload>`
- Encoding tricks: mixed-case schemes, `%0a`, `%09`, double-encoded `%252f`, duplicated keys

If the app exposes a `WKURLSchemeHandler`, try requesting it from attacker-controlled HTML and watch for filesystem or network side effects.

## What "good" looks like

A hardened implementation usually has these properties:

- Top-level WebView navigations are limited to a very small allowlist, ideally exact `https` origins.
- External launches are explicit exceptions (`tel`, `sms`, `mailto`, `facetime`, etc.), not the default path.
- `canOpenURL` is used only as availability logic, not as a security decision.
- Custom schemes are never used as bearer-token transports.
- `WKURLSchemeHandler` paths are canonicalized and strictly mapped to known resources.
- Unknown schemes, browser-internal schemes, and callback parameters are rejected by default.

## References

- [https://mas.owasp.org/MASTG/tests/ios/MASVS-PLATFORM/MASTG-TEST-0077/](https://mas.owasp.org/MASTG/tests/ios/MASVS-PLATFORM/MASTG-TEST-0077/)
- [https://denniskniep.github.io/posts/13-bypass-cve-2024-9956/](https://denniskniep.github.io/posts/13-bypass-cve-2024-9956/)
{{#include ../../banners/hacktricks-training.md}}