fix: any web page can switch the network and destroy the user's custom RPC endpoint, unprompted and unconnected #308
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found by the pre-1.0 deployability audit (#303). Blocker: chain confusion and silent loss of node self-custody, reachable by any page.
src/background/index.js:674-692handleswallet_switchEthereumChainwith noallowedSites/connectedSitescheck and no approval window, unlike every sibling method.src/shared/chainSwitch.js:47-49then overwritesstate.rpcUrlandstate.blockscoutUrlwith the network defaults.Reproduction
The origin was never connected —
eth_accountsfor it does not return an address:Consequence
Two losses.
[TESTNET]banner, so a "test" send spends real ETH.Precondition: the user visits a hostile page. That is the entire precondition.
Definition of done
wallet_switchEthereumChainis gated on the same connection check the signing methods use, and returns4100for an unconnected origin.onChainSwitchdoes not clobber a user-setrpcUrl/blockscoutUrl— remember one endpoint per network instead of overwriting with defaults.make checkgreen.Plan.
wallet_switchEthereumChaingets the sameallowedSites/connectedSitescheck the signing methods use, at the top of the handler (before the same-chain early return), returning{ code: 4100, message: "Unauthorized" }.state.networkEndpoints—{ [networkId]: { rpcUrl, blockscoutUrl } }.onChainSwitch()snapshots the outgoing network's live endpoints into it, then restores the incoming network's remembered pair, falling back to that network's defaults.state.rpcUrl/state.blockscoutUrlstay the live active values, so no reader changes. Invariant: for the active networkstate.rpcUrlis authoritative and the map may be stale; for every other network the map is authoritative.loadState()seedsnetworkEndpoints[networkId]from the storedrpcUrl/blockscoutUrlwhen the key is absent, so a profile written by the current release keeps its custom endpoint as that network's remembered pair.tests/chainSwitch.test.js: unconnected origin refused with4100and the network unmoved; connected origin switches and getschainChanged; a custom RPC survives a switch away and back; the pre-change stored shape loads without loss.Built as planned; PR #313, branch
issue-308-chain-switch-gate, basenext.Gate:
wallet_switchEthereumChaintakes the sameallowedSites/connectedSitescheck the signing methods take, ahead of the same-chain and unsupported-chain answers, and returns{ code: 4100, message: "Unauthorized" }for an unconnected origin.Clobber: endpoints are remembered per network in a new persisted
state.networkEndpoints— the switch snapshots the network being left and restores the network being entered, falling back to that network's defaults.state.rpcUrl/state.blockscoutUrlremain the live values for the active network, so no reader changed. An existing install's storedrpcUrlstays the live endpoint and is additionally adopted as the remembered pair of the network it was stored under, so nothing is lost on first load.Verified: 11 new tests across
tests/chainSwitchGate.test.js(unconnected refused with the state unmoved and nochainChanged; connected switches;4902still returned for a connected origin on an unsupported chain) andtests/networkEndpoints.test.js(custom rpc and blockscout survive a switch away and back, across an extension restart, and from a pre-change stored profile). 9 of the 11 fail against the unfixedsrc/.make checkgreen: 33 suites, 759 tests,test-verify-build18 cases,check-censoredclean, lint executed in the pinned container.