WebView Protocol Handlers

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. For the file-origin / loadFileURL:allowingReadAccessTo: angle, see iOS WebViews. For claimed https handlers, see iOS Universal Links.

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.

<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:

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:

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:

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:

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:

# 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:

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