← Back to all reports

Exported Splash Activity and Unrestricted JS Bridge Enable Session Token Theft via Co-Installed App

Reported Mar 26, 2026
Severity High
Platform Android
Vulnerability Class Exported Component + Token Leak (CWE-926)
Target Type Centralized Crypto Exchange
Impact 90-day session token theft, 15+ authenticated endpoints

The Risk

Any other app installed on the same phone, with no special permissions, could silently steal a crypto exchange user's full session in seconds, with zero taps. The stolen credentials let the attacker view all account balances, see identity verification documents, and stay logged in for 90 days from any computer in the world. Copy-trading and trade execution did not require a second factor, opening a path to drain funded accounts. The malicious app could disguise itself as something harmless like a calculator.

The Vulnerability

A centralized crypto exchange's Android app combined three independent failures that chained into a zero-interaction session theft.

1. Exported splash activity accepts arbitrary URLs

The splash activity was exported and processed an incoming URL passed via an explicit Android intent. There was no validation of the URL's host. Any app on the device could send the splash activity an intent containing a URL pointing anywhere on the internet.

2. WebView loads any URL with no domain check

The router passed URLs starting with http directly to the WebView activity. There was no allowlist of trusted domains and no host replacement on this code path. The WebView simply loaded whatever URL it received.

3. JS bridge unconditionally exposes session tokens

The WebView activity registered a JavaScript bridge (window.control) on every load, with no origin check and no domain restriction. The bridge exposed a sendTokenToWeb() method that returned the user's session token, refresh token, user ID, login name, and device identifier to whatever JavaScript was running on the loaded page.

On top of these client-side issues, the session tokens issued by the backend were not bound to a device or IP server-side, so a stolen token could be replayed from any machine for the full 90-day token lifetime.

The Attack

Delivery

The proof of concept was a small Android app (under 100 lines, zero permissions declared) masquerading as "Crypto Calculator" in the app drawer. On launch the malicious app sent a single explicit intent to the exchange app's exported splash activity, with the data URI set to an attacker-controlled URL.

Execution

  1. The malicious app launches and immediately sends the explicit intent.
  2. The exchange app launches, processes the URL, and loads the attacker page in its WebView.
  3. The JS bridge is registered on load. The attacker's JavaScript calls window.control.sendTokenToWeb().
  4. The bridge returns the session token, refresh token, user ID, login name, and device identifier.
  5. The attacker page POSTs the credentials to the attacker's server with fetch().
  6. The page renders a benign "Verification Complete" view inside the exchange app's own UI.

From the victim's perspective, tapping the malicious app's icon caused a brief launch of the exchange app showing a normal-looking screen, then control returned to the home screen. No prompts, no permissions requests, no dialogs.

Replay from any machine

The stolen tokens were replayed from a separate machine on a different network. The exchange's mobile gateway accepted the request and returned full account data. No device binding, no IP binding.

Endpoints reachable with the stolen token

CategoryData returned
ProfileEmail, user ID, IP, geolocation, invite code, settings
KYCNationality, identity level, UID
SessionsAll devices, IPs, login times, online status
BalancesSpot, futures, copy trading, earn
AccountsAccount types and trading pairs
API keysNames and permissions
NotificationsMessage history

The Impact

Persistent account access

The stolen session token was valid for 90 days, replayable from any IP. This is not a one-shot exfiltration, it is durable account access that persists across the victim's password changes, app reinstalls, and device reboots until the token expires server-side.

Full account intelligence

Fifteen authenticated endpoints returned data with the stolen token, including PII, KYC, balances across all account types, login session lists, API key inventory, and trading preferences.

Fund extraction paths

Copy trading follow endpoints accepted the stolen token without any second-factor check. The only server-side gate was a minimum balance. An attacker could follow their own seeded trader account and execute trades designed to transfer value out of the victim's account. Trade execution on illiquid pairs offered a similar path: place trades that move price against the victim's positions to extract value.

Why "co-installed" is not a mitigating factor

The Android security model explicitly requires apps to validate input on exported components, because any installed app can send intents to them. The vulnerability is entirely in the exchange app's code: an exported activity with no validation, a WebView with no domain allowlist, and a JS bridge that hands out session tokens to any page. The malicious app is just the delivery mechanism, the same way a browser is the delivery mechanism for reflected XSS. Malicious apps reach devices regularly through official app stores, sideloading, enterprise device management, and pre-installed software, none of which involve social engineering of the exchange user.

Remediation

  • Add a strict domain allowlist to the WebView activity. Only load URLs from the company's own domains
  • Validate the WebView's current URL before registering the JS bridge. Better still, gate the bridge methods at call time and refuse to return tokens unless the page is on a trusted host
  • Remove sendTokenToWeb entirely. Session tokens should never be exposed to JavaScript. If the WebView legitimately needs authenticated calls, proxy them through a native API surface that does not return raw credentials
  • Validate URLs received by the exported splash activity before passing them to the router. A startsWith("http") check is not a security control
  • Bind session tokens to device and IP claims server-side. Reject replay from unknown contexts
  • Shorten token lifetime. A 90-day session JWT is excessive for a financial application
  • Require a second factor on copy-trading configuration changes and on trades over a configurable threshold