Intent URI Bypass Loads Attacker Page in Exchange WebView, JS Bridge Returns Auth Token For One-Click Account Takeover
The Risk
Any logged-in user of the exchange's app could have their account taken over by tapping a single link in any browser, email, or chat. The link silently opens the app, loads an attacker page inside the in-app browser, and that page reads the user's login token by asking the app's own internal JavaScript helper for it. The token grants the attacker the same access the user has, including reading balances and placing orders that move funds. The whole chain finishes in under two seconds and the user only sees a "Verification Complete" screen.
The Vulnerability
The app had two deep link entry points that ended up loading URLs in the same in-app browser, but only one of them validated the URL.
- The custom-scheme entry point (
exchange://web?url=...) validated the URL against a domain allowlist of the exchange's own domains. - The intent-extra entry point read a
urlstring extra delivered through Android'sintent://URI scheme. The router only checked that the value started with "http" and skipped the domain validation entirely. It then opened the in-app browser with the JavaScript bridge enabled.
Because Android's intent:// URI scheme lets any web page deliver string extras to an exported activity, an attacker could fire the bypassing path from any HTTPS site with no co-installed malicious app required.
Once the attacker's page loaded inside the WebView, the in-app JavaScript bridge exposed a getToken method that returned the user's authentication token from local storage with no origin check. The bridge was reachable through a window prompt hook used by the app's WebView client.
The Attack
- The attacker hosts a delivery page with an
intent://link whoseS.urlextra is the attacker's own URL. A separate attack page hosts the bridge exploit JavaScript. - The victim, logged into the app, taps the link in any browser, email, or messaging app.
- The browser fires an intent that delivers the attacker URL as a string extra to the app's main activity.
- The internal router sees the URL starts with "http" and opens it in the in-app browser with the JavaScript bridge attached. The domain allowlist does not run on this path.
- The attack page calls
window.promptwith a JSON payload that the bridge interprets as a request to callgetToken. The bridge returns the auth token. - The token is exfiltrated to the attacker server. The page then uses it server-side to pull the victim's account info. The victim sees a "Verification Complete" screen.
Confirmed token power
With the captured token the attacker reached, on the live API:
| Capability | Endpoint behaviour |
|---|---|
| Read account identity (user ID, masked email, last login IP, KYC status) | Returned full PII payload |
| List spot accounts and balances | Returned all balances |
| Read full DEX wallet portfolio | Returned 136 currencies and total value |
| Read futures account, margin, unrealised P&L | Returned, confirming token works across sub-domains |
| List OTC merchant profile and saved bank accounts | Returned |
| Place spot order | Returned parameter validation error, not 401, confirming write access |
| Place DEX swap order | Returned parameter validation error, not 401 |
| Add or modify saved payment method | Accepted by API |
None of the tested endpoints required additional 2FA at the API layer. The token alone was sufficient to read account data and submit write operations.
The Impact
For any funded account, the attacker can drain funds via unfavourable trades placed against an attacker-controlled counter-account, create P2P trades, and add their own bank account as a saved payment method. Every read endpoint that returns PII is also reachable, which on its own is a serious incident for a regulated exchange.
The delivery vector is a single link, so the attack scales without any infrastructure beyond a phishing domain. Any logged-in user who taps the link is vulnerable, no matter where the link is delivered or how the app was installed.
Remediation
- Apply the same domain allowlist to every code path that opens the in-app browser, including the intent-extra entry point. Reject any URL that is not on the allowlist.
- Treat the in-app JavaScript bridge as privileged. Only inject it for pages whose origin is on the trusted allowlist, and check the origin again inside every bridge method.
- Make
getTokenand any other sensitive bridge methods refuse to respond to non-trusted origins, even if the bridge is somehow attached. - Add
android:autoVerify="true"to all custom-scheme intent filters and prefer App Links over custom schemes for any URL that loads in a privileged WebView. - Bind issued tokens to a device or session so a stolen token cannot be replayed from another network or device, and require step-up confirmation for sensitive actions like adding a payment method.