Threat Research Meets AI: How We Found 13 UXSS Vulnerabilities in Android WebView
We found the first 7 UXSS vulnerabilities through manual reverse engineering. Then, we formalized the patterns and pointed Novee at 20 Android apps. It found five more, plus a critical account takeover.
This post describes security research conducted under responsible disclosure. Don’t test systems you don’t own or have explicit permission to assess.
Introduction
This post details what happens when expert security research and the Novee offensive AI work together on the mobile attack surface.
This research examines one vulnerability class, Universal Cross-Site Scripting (UXSS) in Android WebView. It details how we took an instance found manually, synthesized it into a pattern Novee hunts for automatically, and set it loose on twenty different Android apps to find critical bugs.
Those 20 Android apps span browsers, OEM preinstalled apps, messaging platforms, and e-commerce apps. Not only did we find the UXSS pattern in 12 of them, but we also discovered a critical account takeover that needed no UXSS at all. Billions of installations potentially affected, all responsibly disclosed.
What Is UXSS, and Why Should You Care?
Regular XSS lets an attacker run JavaScript on a vulnerable website. Universal XSS is a different order of problem.
It means an attacker can execute JavaScript on any origin of their choosing: your bank, your email, your corporate intranet. The bug lives in the application doing the rendering, not in the website itself. The victim visits an attacker-controlled page, or taps a single link, and the attacker gains full script execution on whatever sensitive site they decide to target.
On Android (where WebView is the backbone of in-app browsers, embedded web content, and hybrid apps) UXSS translates into session hijacking on any website (reaching httpOnly cookies in some cases), full account takeover of services rendered inside WebView, a complete CSP bypass because the injection comes from the native layer, and DOM manipulation or phishing on trusted domains, sometimes with near-zero user interaction.
The Manual Research: Finding the First Seven Bugs
We began where good security research usually does: hands on the code, manually reversing Android applications to understand how WebView bridges actually work.
Picture the JavaScript bridge as a two-lane road:

The inbound road (web calling native), has been studied to death. Developers add origin checks, token validation, and domain whitelists; OWASP documents it; static analysis tools flag it.
The outbound road (native replying to web) has been almost entirely ignored.
The critical insight is that when native code calls evaluateJavascript() or loadUrl(“javascript:…”), it executes against whatever page is currently loaded in the WebView. If the page has changed since the original request was made, the response lands on the wrong origin. We confirmed this across seven manually discovered UXSS vulnerabilities in major applications and recognized it wasn’t a collection of one-offs, but a systematic pattern we could formalize and automate.
The Pattern: TOCTOU on Origin
We manually discovered a Time-of-Check, Time-of-Use (TOCTOU) race on origin:
- Check: The attacker’s page passes all inbound validation (whitelists, bridge authentication, origin checks)
- Gap: The app performs asynchronous work: an IO operation, a dialog display, a hardware event listener
- Navigate: During the async gap, the attacker navigates the WebView to a victim origin
- Use: The native callback fires and blindly calls evaluateJavascript() on whatever page is now loaded – the victim’s page

This isn’t a timing race you need to win in microseconds. Many of these async gaps are user-triggered: a dialog dismissal, a volume button press, an app switch. The attacker has all the time in the world.
Seven bugs. Two distinct families. Now we had formalized patterns we could teach Novee.
Scaling the Findings With Novee
With the patterns formalized from our manual research, we encoded the detection logic into Novee. It doesn’t just grep for evaluateJavascript calls; it reasons about the semantic pattern, finding outbound reply paths where native code sends data back to web content, tracing whether those paths validate the current origin at execution time, and identifying async gaps an attacker could use to swap the page between call and callback.
Family 1: Bridge Reply-Path UXSS
These are the core of what Novee detected, the same pattern we found by hand, showing up across different applications in four variations.
Variation A: async native callbacks. A bridge method does its work on a background thread, stores the JavaScript callback as a string, and executes it when the work completes, without re-checking whether the page has changed.
// Pseudocode - pattern Novee identified in [REDACTED] @JavascriptInterface public void someAsyncMethod(String params, String callback) { // ✅ Origin check happens here (or is bypassed) executor.execute(() -> { Object result = doExpensiveWork(params); // ❌ No origin check before reply webView.evaluateJavascript( "javascript:" + callback + "('" + result + "')", null); }); }
In one case, a deep link loaded any URL into a fresh “instant app” WebView with the bridge pre-registered, and the domain whitelist was bypassed because an isFirstUrl() check (based on !webView.canGoBack()) returned true on a WebView with no back history. A pagehide handler then fired the bridge call at the exact moment of navigation commit, so the callback landed on the victim page.
Variation B: persistent event listeners. Some bridge methods register listeners that fire repeatedly (volume changes, visibility changes, network updates) and survive navigation. The attacker registers a listener with a malicious callback on their own page, navigates to a victim origin, and the next event fires the callback there.
// Pseudocode - pattern Novee identified in [REDACTED] @JavascriptInterface public void addVolumeChangeListener(String params, String callbackId) { audioManager.registerCallback(new VolumeCallback() { @Override public void onVolumeChanged(int level) { // ❌ Fires on whatever page is loaded NOW webView.evaluateJavascript(buildCallback(callbackId, level), null); } }); }
The trigger isn’t timing-sensitive at all. In one app the native dispatch used String.format(“…dispatchResult(‘%s’,’%s’,%s)…”, callbackId, …) with an attacker-controlled, unsanitized callback ID, so a single-quote injection broke out of the string:
Exploit flow:
1. Deep link loads attacker page in the app's BridgeWebView
2. _nativeBridge.newCall() registers volume + visibility listeners
3. Callback IDs contain single-quote injection: x','',true);PAYLOAD//
4. WebView navigates to google.com - listeners persist (not cleared on nav)
5. User presses a volume button or switches apps
6. Dispatch builds: bridgeCore.dispatchResult('x','',true);PAYLOAD//',...);
7. The // comments out the rest - PAYLOAD executes on google.com's origin
Variation C: stored callbacks with ambient triggers. A bridge call stores a callback for a future user action (dismissing a dialog, finishing a login), the attacker triggers the action after navigating away, and the stored callback fires on the new page.
// Pseudocode - pattern Novee identified in [REDACTED] public void openLoginDialog(String dismissCallback) { this.storedDismissCallback = dismissCallback; // persists in a native singleton showLoginOverlay(); } public void onLoginDismissed() { // ❌ Original page may be gone webView.evaluateJavascript(storedDismissCallback, null); }
Novee reached this two different ways in practice: once via a reflected XSS on a whitelisted first-party tracking domain that unlocked restricted bridge methods, and once by capturing a per-session UUID bridge secret with a Proxy on the injected __nav object, then replaying it after navigation. In both, the callback dispatcher skipped the origin check (an empty URL guard field in one, dead-code scheme filtering in the other).
Variation D: unrestricted userscript installation. One browser exposed a bridge method that let any page install a persistent userscript with an attacker-chosen @match pattern, no confirmation, no origin restriction, surviving browser restarts. After a single visit, every page the user loads runs the injected script. The same bridge also exposed methods to read all cookies for any domain including httpOnly, make authenticated cross-origin requests with the victim’s cookies, and write cookies into any domain. Novee flagged it as one page visit resulting in permanent, total browser compromise.
Family 2: Navigation-Path UXSS
The second family exploits how apps handle intents, deep links, and URL loading to inject javascript: URIs that execute on an existing page.
Variation A: javascript: as a browsable scheme. Some Chromium-based browsers declare javascript: as browsable in their manifest, so another app can send an intent that runs script on the current tab.
<intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="javascript" /> <!-- ← UXSS sink --> </intent-filter>
A newline (%0a) defeats the javascript:// comment line, and the intent:// URI can be delivered from a normal HTTPS link via a 302 redirect or a <base href=”intent://”> trick. Because loadUrl(“javascript:…”) runs below Content Security Policy, this bypasses CSP entirely, even on sites with script-src ‘none’.
intent://%0aalert(document.domain)#Intent;scheme=javascript;end
Variation B: browser_fallback_url injection. Several apps accept javascript: URIs as the fallback when an intent’s target package isn’t installed, then load that URI on the current page.
intent://x#Intent;scheme=app;package=com.not.installed; S.browser_fallback_url=javascript:alert(document.domain);end
In one messaging app this chained through a child WebView opened via window.open(): the parent set the child’s location to the intent://, the fallback fired loadUrl(“javascript:…”), and by pointing the victim at the app’s own web session, the chain achieved full account takeover of the messaging account from a single link in a chat.
Variation C: cross-app composition. This was Novee’s most surprising finding. An app with no vulnerability of its own becomes exploitable because of what other installed apps declare. When any app declares javascript: as browsable, Android’s chooser includes every browser, and a browser that doesn’t declare the scheme still processes it if the user picks it. A three-line manifest in a throwaway app is enough:
<activity android:name=".Stub" android:exported="true"> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="javascript" /> </intent-filter> </activity>
Exploit flow (victim viewing google.com in a target browser): 1. Victim clicks an https://...google.com/url?q= redirect link 2. Attacker server responds 302 → intent://%0aPAYLOAD#Intent;scheme=javascript;end 3. PackageManager resolves the javascript: scheme via the stub app 4. Android shows a chooser - victim picks the target browser (natural choice) 5. Target browser loads javascript://... into the current tab (google.com) → UXSS
In practice the stub isn’t even needed, since several widely pre-installed browsers already declare javascript:, making every other Chromium browser on the device exploitable just by being present.
We’re not aware of prior work documenting this cross-app composition vector.
Variation D: intent extras and deep link handlers. Several apps read a URL from an intent extra or deep link parameter and pass it straight to loadUrl() with no scheme validation.
@Override protected void onNewIntent(Intent intent) { String url = intent.getStringExtra("KEY_URL"); if (url != null) webView.loadUrl(url); // ❌ javascript: URIs execute as script }
In one browser, shouldOverrideUrlLoading dispatched intent:// URIs even from hidden iframes with no user gesture, which combined with a singleTask onNewIntent handler to give near-zero-click UXSS. It was also reachable locally, making it a privilege-escalation vector from any app or ADB:
adb shell "am start -n com.example.smartbrowser/.ui.main.MainActivity \
--es KEY_INTENT_URL 'javascript:alert(document.domain)'"
Beyond UXSS: Bridge Authentication Bypass → Account Takeover
Novee also uncovered a critical account takeover in a major e-commerce app that required no UXSS at all. The bridge authentication used a regex to check the calling page’s domain:
String pattern = "https?://.*(\\.trusted-domain)\\.com/.*";
The regex is unanchored, so it matches the trusted domain anywhere in the URL. A page hosted at https://evil.com/.trusted-domain.com/steal passes the check and gains full access to authenticated bridge methods, including one returning the user’s access token, email, and account IDs.
Exploit flow: 1. Victim clicks https://evil.com/ (SMS, email, ad) 2. Landing page deep-links into the app → app opens 3. App loads https://evil.com/.trusted-domain.com/exfil in a privileged WebView 4. Auth regex matches (the .* eats "evil.com/" and matches ".trusted-domain.com/") 5. Bridge access granted → getUserInfo returns access_token, email, member_id 6. Data exfiltrated via image beacon; access_token gives full API access
The correct regex uses [^/]* instead of .* to keep the match on the host. One tap, silent, full account takeover, from a single bad regex.
The Impact
Prior WebView research has overwhelmingly focused on the inbound path: who can call native bridges and whether origin checks exist on @JavascriptInterface methods. Our manual work showed that even apps with perfect inbound security can carry trivially exploitable UXSS through their outbound reply path, and Novee now checks this systematically on every mobile app it tests.
We didn’t stop at one bug and a rule to match it. We taught Novee to reason about the TOCTOU-on-origin pattern semantically, understanding async gaps, callback storage, and origin validation at a conceptual level. The pattern fits in a single sentence, “does the callback check the origin before executing?”, yet Novee applied it across wildly different codebases and turned up a dozen distinct vulnerabilities, many triggered by ambient user actions rather than microsecond races, and every one of them bypassing Content Security Policy completely. The cross-app composition finding goes a step further, breaking the assumption that an app’s security can be evaluated in isolation.
Recommended Defenses
For application developers using WebView:
- Treat every evaluateJavascript() and loadUrl(“javascript:…”) as a potential UXSS sink. Audit every call site and trace backwards to the trigger.
- Bind callbacks to the originating document. Invalidate stored callbacks when the page navigates, using onPageStarted() or navigation callbacks to clear pending state.
- Re-validate origin at use time, not just at call time. The page may have changed between call and reply.
- Validate browser_fallback_url schemes. Allow only http:// and https://, and never pass unvalidated URLs to loadUrl().
- Never declare javascript: as a browsable scheme in your AndroidManifest.
- Migrate to WebViewCompat.addWebMessageListener() (API 33+), which drops replies when the originating frame navigates away and is immune to this pattern by design.
- Test cross-app composition. Your attack surface changes based on what else is installed, so test with apps that declare unusual intent filters.
If you maintain an Android application that uses WebView bridges or processes javascript: URIs from intents, audit your outbound reply paths. The inbound road has guardrails. The outbound road, in most apps we tested, has none.
The mobile testing that produced these findings is live in the Novee platform. Every customer gets the patterns Novee learned here, the TOCTOU-on-origin detection, the navigation-path analysis, and the bridge authentication auditing, applied automatically to their Android apps. Findings arrive proven, paired with stack-specific remediation, and Novee retests after each fix to confirm the risk is closed, the same closed-loop flow we run on web and on the APIs behind both, all in one dashboard.
Novee is one AI pentesting platform for the whole attack surface
Most breaches don’t respect the boundaries security teams draw on their architecture diagrams. An attacker who can’t get through the web front end will pivot to the mobile app, and one who can’t break the mobile app will go straight at the API sitting behind both. Novee tests all of it from a single platform: the backend APIs, the web applications, and the mobile apps, with every finding tracked in one dashboard.
How it works. Novee brings the AI-powered platform we already run for web over to mobile: users upload an APK, and the agent handles the rest, pairing static analysis of the package with live runtime testing in one assessment. It maps the full Android attack surface, activities, services, deep links, content providers, intents, WebViews, and broadcast receivers, with every assessment mapped to OWASP MASTG and MASVS coverage across storage, crypto, authentication, network, platform, code, and resilience. Every finding comes proven, with exploit evidence and replication steps, and each fix is tailored to the stack and retested automatically to confirm the risk is closed.
The Asset Intelligence Model (AIM) builds a living picture of each mobile app, its workflows, permissions, APIs, and business logic, so testing sharpens each cycle rather than resetting to zero. Because mobile and web share one platform, you get unified risk tracking alongside MASTG-mapped evidence that holds up for SOC 2, ISO 27001, and customer due diligence.
Let us show you what your attackers already see. Explore the Novee platform.