Issue #1049
When you embed web content in a WKWebView, you often want it to stay on a set of trusted domains. A login flow that wanders off to an arbitrary site, or a help page that turns into an open browser, is both a security and a product problem. There are two ways to control this. One is declarative, handed to WebKit through your Info.plist. The other lives in your navigation code, where you decide each request yourself.
App-bound domains in the Info.plist
The first approach is App-Bound Domains, introduced in iOS 14. You declare the domains your app trusts as a WKAppBoundDomains array in the app target’s Info.plist.
<key>WKAppBoundDomains</key>
<array>
<string>example.com</string>
<string>auth.example.com</string>
</array>
Declaring the key is not enough on its own. You also opt a web view in by setting a flag on its configuration.
let configuration = WKWebViewConfiguration()
configuration.limitsNavigationsToAppBoundDomains = true
let webView = WKWebView(frame: .zero, configuration: configuration)
Now WebKit enforces the list for you. A main-frame navigation to a host outside the array is refused before your code ever sees it, and the console logs a message about ignoring a request to navigate away from an app-bound domain. The check sits below your delegate, so it also covers redirects and window.open that your own logic might miss.
You get 10 entries, and anything past that is ignored. The list is also static, baked into the build, so changing it means shipping an update. Treat the slots as precious and list the exact hosts you need rather than hoping a parent domain covers everything.
Allowing domains in code
The second approach is to validate each navigation yourself in WKNavigationDelegate. You keep your own list and check the host of every request.
final class BrowserViewController: UIViewController, WKNavigationDelegate {
private let allowedDomains = ["example.com", "auth.example.com"]
func webView(
_ webView: WKWebView,
decidePolicyFor action: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
) {
guard let host = action.request.url?.host else {
decisionHandler(.cancel)
return
}
decisionHandler(isAllowed(host) ? .allow : .cancel)
}
private func isAllowed(_ host: String) -> Bool {
allowedDomains.contains { host == $0 || host.hasSuffix("." + $0) }
}
}
The interesting part is isAllowed. A plain equality check forces you to spell out every host, which is painful when an identity provider bounces through several subdomains. The suffix check fixes that. Listing example.com matches example.com itself and any *.example.com, so login.example.com and api.example.com pass with a single entry. The leading dot in "." + $0 is what keeps it honest, since notexample.com does not end in .example.com and is correctly rejected.
Because the list is just data, you can load it from a remote config and adjust the allowed domains without an app release. You are not bound to ten entries either.
Start the conversation