← Back to all reports

Exchange App DApp Browser Deep Link to One-Tap Permit Drain

Reported Apr 17, 2026
Severity High
Platform Android
Vulnerability Class Exposed Sensitive Intent + Stale Allowlist (CWE-926, CWE-1188)
Target Type Centralized Crypto Exchange
Impact One-tap unlimited ERC-20 allowance capture

The Risk

Anyone holding a wallet inside a major centralized crypto exchange's Android app could be drained with a single tap on a phishing link. The link opened the exchange app's wallet browser on an attacker page that carried no warning banner, because the exchange's "trusted sites" list still pointed at domains whose owners had let them lapse. The attacker registered one of those abandoned domains, a normal commercial registration, and bought a free certificate. With one signature prompt the victim's tokens were transferable to the attacker, no further interaction needed.

The Vulnerability

This is a different chain to the one previously documented on a wallet app's deep link bridge. The root primitive here is an exposed exchange deep link route plus a stale on-device safety classifier list. Two issues compound:

1. Exposed deep link route skips the host allowlist

The exchange app's exported splash activity accepted a browser-reachable deep link with two parameters: a route identifier pointing to the in-app DApp browser, and a URL parameter consumed verbatim. This route fed the URL directly into a Flutter-backed WebView with the full Web3 signing bridge attached. A parallel deep link route into the legacy WebView did pass through a host matcher, but the new Flutter DApp route did not. Any host could be loaded with the full wallet bridge.

2. Stale on-device safety whitelist suppresses warnings

The app shipped a 676-entry local whitelist used to classify a host as a known DApp. Hosts on the list were marked as a safety site and the "Unknown Website" warning banner was suppressed. Several entries pointed at domains whose owners had let the registration lapse and which were now available at any standard registrar. Buying one of those domains and pointing it at an attacker server was enough to inherit the green-light classification.

Additional pre-population issues

On every DApp page load, the in-app browser pre-populated window.solana.publicKey, window.tronWeb.defaultAddress, and silently auto-resolved eth_requestAccounts on whitelisted hosts. No connect prompt was shown. The attacker page received the victim's three wallet addresses with zero user interaction.

The Attack

  1. The attacker sweeps the on-device whitelist for domains in NXDOMAIN or redemption state. Six were available at standard registrars; another four were due to drop within 75 days.
  2. They register one of the available domains and issue a Let's Encrypt certificate for the exact host listed in the whitelist (the www. prefix matters where the entry uses one).
  3. They host a phishing landing on the registered domain that auto-fires the deep link with the route identifier and a URL parameter pointing back to the same domain's attack page.
  4. The victim opens the link in Chrome on a stock Android device with the exchange app installed and a Web3 wallet active. Chrome hands off to the exchange app via the exposed deep link.
  5. The Flutter DApp browser opens directly on the attacker page. No warning banner. The browser pre-populates Solana and Tron addresses and silently resolves the Ethereum address. All three are exfiltrated server-side before the victim taps anything.
  6. The attack page invokes eth_signTypedData_v4 with an unlimited permit payload for a target ERC-20 (USDC, USDT, DAI, WETH, WBTC, or any other EIP-2612 token). A signature dialog appears wrapped in the exchange's normal chrome.
  7. One tap to confirm and a biometric prompt later, the attacker holds a valid signature granting the attacker spender unlimited allowance on the victim's balance.
  8. The attacker submits permit(victim, attacker, max_uint256, ...) followed by transferFrom(victim, attacker, balance) on chain. The wallet is drained in two transactions with no further victim interaction.

The Impact

Confirmed end to end on the researcher's own wallet on a stock Android device:

  • Silent capture of the victim's Ethereum, Tron, and Solana addresses with zero taps after the link opened.
  • Captured a valid unlimited permit signature for USDC after one confirm tap and biometric.
  • Same primitive applies to any EIP-2612 ERC-20 token (USDT, DAI, WETH, WBTC).
  • Equivalent primitives available for SPL tokens via the Solana bridge and TRC-20 tokens via the Tron bridge.
  • Affects every user of the exchange's Android app with an active Web3 wallet, on stock devices with no root.

Remediation

  • Apply the existing host matcher to the URL parameter on the Flutter DApp route, matching the protection on the legacy WebView route.
  • Cull and continuously audit the on-device DApp safety whitelist. Auto-remove entries on NXDOMAIN, redemption status, or change of registrar ownership.
  • Decode signTypedData_v4 payloads in the signature dialog. Render permit, approve, and setOperator calls with field-level breakdowns and an explicit warning when the value is unlimited.
  • Gate window.solana.publicKey and window.tronWeb.defaultAddress behind an explicit user-approved connect() step. Both are currently pre-populated on every page load.
  • Drop wallet-spoofing flags on the Ethereum provider object. Expose only the exchange's own wallet identifier.
  • Require a signature-level android:permission on the launching intent filter so only same-signed apps can fire it.