← Back to all reports

WebView contains() Domain Bypass to Full Account Takeover

Reported Feb 24, 2026
Severity High
Platform Android
Vulnerability Class Improper Input Validation (CWE-184)
Target Type Crypto Exchange
Impact Full account takeover via session token theft

The Risk

An attacker could install a harmless-looking app on a user's phone that silently steals their login session from a cryptocurrency exchange. Once stolen, the attacker gains full access to the victim's account for up to 30 days, including the ability to view personal information, check balances, and place trades. The victim sees nothing suspicious, and changing their password does not stop the attacker.

The Vulnerability

1. Exported activity accepts attacker intents

The app's main notification-handling activity was exported and processed notification payloads from any installed app on the device. There was no permission check, no signature verification, and no caller validation. Any app could send a crafted intent containing a URL, which the activity stored in an internal observable. When that observable fired, it opened the URL in the app's WebView.

2. Substring domain check instead of exact match

Before injecting authentication tokens into HTTP headers, the WebView activity checked whether the URL's hostname was legitimate. The check used a contains() call against the exchange's domain name:

hostname.contains("exchange.com")  // vulnerable

This meant any domain containing that string as a substring would pass. An attacker could register domains like:

  • fakeexchange.com - contains the target domain
  • exchange.com.evil.com - contains the target domain
  • exchange.com.attacker.com - contains the target domain

The correct check should have been an exact match or an endsWith() check anchored with a dot separator:

hostname.equals("exchange.com") || hostname.endsWith(".exchange.com")  // secure

3. Token injection into attacker-controlled requests

When the domain check passed, the WebView loaded the URL with the user's JWT access token and refresh token injected as custom HTTP headers. These headers were sent directly to the attacker's server along with additional metadata including the user's language, currency preference, and app theme.

The Attack

  1. The attacker registers a domain containing the exchange's name as a substring and sets up an HTTPS capture server
  2. A proof-of-concept app is built and disguised as a utility (e.g., "Crypto Calculator"). It passes automated app store safety checks
  3. When the victim opens the attacker's app, it sends a crafted intent to the exchange app's exported activity
  4. The exchange app opens the attacker's URL in its internal WebView. The contains() check passes
  5. The WebView injects the victim's JWT access token and refresh token as HTTP headers to the attacker's server
  6. The attacker's server captures the tokens and immediately uses them to call the exchange's API

The entire chain executes without any user interaction beyond opening the attacker's app. No permissions are requested, no dialogs appear, and the exchange app briefly opens before returning to the foreground app.

The Impact

Using the stolen tokens, the following API actions were confirmed against a live test account:

ActionResult
Read full profile (name, email, country)Confirmed
View wallet balances (spot, futures, earn)Confirmed
View per-coin balancesConfirmed
View open orders and trade historyConfirmed
Place spot market ordersAccepted (blocked by KYC, not auth)
Place futures ordersAccepted (blocked by quiz, not auth)
Convert assets between walletsAccepted (no 2FA required)
Withdraw fundsBlocked by 2FA

Persistence and stealth

The refresh token was valid for 30 days and used stateless JWT validation, meaning it survived password changes. An attacker who captured the token once could maintain access for a full month with no way for the victim to revoke it. Testing was performed across two devices and two Android versions with a 100% success rate across five capture attempts.

Remediation

  • Replace contains() domain checks with exact match or properly anchored endsWith() checks
  • Validate intent sources on exported activities using signature-level permissions or caller package verification
  • Only inject authentication tokens for URLs on an explicit allowlist of first-party domains
  • Bind refresh tokens to device or session context server-side so stolen tokens cannot be replayed from other machines
  • Implement token revocation so password changes invalidate all active sessions

The vendor confirmed the fix in a subsequent release. The domain validation was updated to use exact match and endsWith(), and tokens are no longer injected for non-first-party domains.