Security hardening for the Android app; no offchain lookups; quiet-period consent buttons #5

Merged
backmeupplz merged 2 commits from security-fixes into main 2026-09-25 17:08:57 +00:00
backmeupplz commented 2026-09-25 04:12:59 +00:00 (Migrated from github.com)

Fixes from a security review of the Android app. The details stay out of this public PR on purpose (CONTRIBUTING.md); this describes what changes.

Android

  • Sites and the wallet no longer share a WebView renderer or storage. The browser WebView gets a profile of its own, so sites run in their own renderer process with their own storage. The app now requires profile support and WebView 140+. If only the sites' renderer dies, the browser is rebuilt and the wallet keeps its session and approvals.
  • Sites can't open JavaScript dialogs, which Android would draw over the wallet.
  • Messages from pages are validated (text only, size-capped) before anything reads them. The wallet page answers malformed ones with a JSON-RPC parse error.
  • While the wallet is showing:
    • its screens are kept out of screenshots, recordings and the recents view (FLAG_SECURE);
    • other apps' overlays are hidden, and touches passed through obscuring windows are ignored;
    • it is excluded from autofill, and keyboards are told not to learn what's typed (IME_FLAG_NO_PERSONALIZED_LEARNING, autocorrect="off" on the seed box);
    • on Android 14+ it is marked sensitive to accessibility services that aren't accessibility tools;
    • a blue band sits behind the status and address bars, which web content can't draw.
  • Locking: the wallet locks after a minute away from the app (screen off included), measured with the uptime clock, which changing the date doesn't move. The page's own auto-lock uses a monotonic clock.
  • Approvals: leaving the wallet rejects what's waiting there, as closing the extension's approval window does. Its consent buttons re-arm when it comes back on screen.
  • Address bar:
    • shows only the host, cut from the left, for the committed page;
    • input shaped like a seed phrase or private key is refused rather than searched;
    • addresses containing backslashes are treated as searches, and the wallet's host is blocked in the browser outright.
  • Backups: no cloud backup or device-to-device transfer of app data (dataExtractionRules).
  • Fingerprint: the key requires an unlocked device; a missing Keystore key or an error without a message no longer leaves the prompt stuck or looping.
  • Other:
    • taskAffinity="";
    • a Content-Security-Policy on the wallet page;
    • favorites labeled by host.
  • Build and signing:
    • Release APKs come out of Gradle unsigned and are signed with apksigner separately, so no build tool (Gradle, plugins, the npm build) sees the release key or its password.
    • Gradle downloads are checked against gradle/verification-metadata.xml (sha256), and Google's repository is limited to Google's own groups.

Extension too

  • viem's EIP-3668 offchain lookups are off (ccipRead: false).
  • Approve and Send come on only after 800 ms without taps or clicks, restarted by every tap. They are marked with aria-disabled, so taps on the button itself count.

Checks

  • npm test passes, including new checks: the app's lock message, a parse-error reply for malformed requests, and ccipRead === false. Mutation checks confirm that the new assertions fail when the behavior is reverted.
  • npm run build passes, and ./gradlew assembleDebug assembleRelease lintDebug passes with dependency verification on.
  • The wallet page runs in Chromium with the new CSP and shows no violations. The quiet-period arming holds Approve through repeated taps, a click during the wait doesn't approve, and visible re-arms it.
  • On a OnePlus 8 (Android 13, WebView 150), with a debug build of this branch:
    • The site and the wallet run in two separate renderer processes.
    • The earlier crash (a page sending an ArrayBuffer) no longer takes the app down.
    • prompt, alert and confirm return immediately, with no dialog shown.
    • A malformed message gets a -32700 reply.
    • A screenshot with the wallet showing comes out empty; the browser can still be captured.
    • The wallet's inputs carry IME_FLAG_NO_PERSONALIZED_LEARNING.
    • https://app.uniswap.org@example.org/ shows as example.org.
    • A seed-shaped input in the address bar is refused, with no navigation or search.
    • Leaving the wallet rejects a pending connect request (4001).
    • After 70 s in the background the wallet locked itself, in the same process.

🤖 Generated with Claude Code

Fixes from a security review of the Android app. The details stay out of this public PR on purpose (CONTRIBUTING.md); this describes what changes. ## Android - **Sites and the wallet no longer share a WebView renderer or storage.** The browser WebView gets a profile of its own, so sites run in their own renderer process with their own storage. The app now requires profile support and WebView 140+. If only the sites' renderer dies, the browser is rebuilt and the wallet keeps its session and approvals. - **Sites can't open JavaScript dialogs**, which Android would draw over the wallet. - **Messages from pages are validated** (text only, size-capped) before anything reads them. The wallet page answers malformed ones with a JSON-RPC parse error. - **While the wallet is showing:** - its screens are kept out of screenshots, recordings and the recents view (`FLAG_SECURE`); - other apps' overlays are hidden, and touches passed through obscuring windows are ignored; - it is excluded from autofill, and keyboards are told not to learn what's typed (`IME_FLAG_NO_PERSONALIZED_LEARNING`, `autocorrect="off"` on the seed box); - on Android 14+ it is marked sensitive to accessibility services that aren't accessibility tools; - a blue band sits behind the status and address bars, which web content can't draw. - **Locking:** the wallet locks after a minute away from the app (screen off included), measured with the uptime clock, which changing the date doesn't move. The page's own auto-lock uses a monotonic clock. - **Approvals:** leaving the wallet rejects what's waiting there, as closing the extension's approval window does. Its consent buttons re-arm when it comes back on screen. - **Address bar:** - shows only the host, cut from the left, for the committed page; - input shaped like a seed phrase or private key is refused rather than searched; - addresses containing backslashes are treated as searches, and the wallet's host is blocked in the browser outright. - **Backups:** no cloud backup or device-to-device transfer of app data (`dataExtractionRules`). - **Fingerprint:** the key requires an unlocked device; a missing Keystore key or an error without a message no longer leaves the prompt stuck or looping. - **Other:** - `taskAffinity=""`; - a Content-Security-Policy on the wallet page; - favorites labeled by host. - **Build and signing:** - Release APKs come out of Gradle unsigned and are signed with `apksigner` separately, so no build tool (Gradle, plugins, the npm build) sees the release key or its password. - Gradle downloads are checked against `gradle/verification-metadata.xml` (sha256), and Google's repository is limited to Google's own groups. ## Extension too - viem's EIP-3668 offchain lookups are off (`ccipRead: false`). - Approve and Send come on only after 800 ms without taps or clicks, restarted by every tap. They are marked with `aria-disabled`, so taps on the button itself count. ## Checks - `npm test` passes, including new checks: the app's `lock` message, a parse-error reply for malformed requests, and `ccipRead === false`. Mutation checks confirm that the new assertions fail when the behavior is reverted. - `npm run build` passes, and `./gradlew assembleDebug assembleRelease lintDebug` passes with dependency verification on. - The wallet page runs in Chromium with the new CSP and shows no violations. The quiet-period arming holds Approve through repeated taps, a click during the wait doesn't approve, and `visible` re-arms it. - On a OnePlus 8 (Android 13, WebView 150), with a debug build of this branch: - The site and the wallet run in two separate renderer processes. - The earlier crash (a page sending an `ArrayBuffer`) no longer takes the app down. - `prompt`, `alert` and `confirm` return immediately, with no dialog shown. - A malformed message gets a `-32700` reply. - A screenshot with the wallet showing comes out empty; the browser can still be captured. - The wallet's inputs carry `IME_FLAG_NO_PERSONALIZED_LEARNING`. - `https://app.uniswap.org@example.org/` shows as `example.org`. - A seed-shaped input in the address bar is refused, with no navigation or search. - Leaving the wallet rejects a pending connect request (4001). - After 70 s in the background the wallet locked itself, in the same process. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.
No description provided.