harden: verify the signed transaction against what the popup displayed (closes #216)
All checks were successful
check / check (push) Successful in 32s

The signed artifact was compared with the dApp's request object, so every
field the dApp left out — normally the nonce, the gas limit and every fee
field, because populateTransaction() filled them in the popup — was checked
by nothing but the absolute ceilings. A bare transfer at the fee ceiling
hands the validator 2.1 ETH. The ceilings were never the defect: the thing
being verified was not the thing the user approved.

The transaction is now populated in the background, before the approval
window opens, and that populated object is what is displayed, what the popup
signs, and what the artifact is verified against. Every consequential field
is compared exactly.

- src/shared/approvalTx.js populates the request through a VoidSigner over
  the configured RPC and serializes the result to the fields its type
  serializes, as hex quantities that survive the JSON messaging boundary.
  Fields the wallet does not act on are dropped before ethers sees the
  page's object.
- Population failure raises no approval and opens no window: the error goes
  back to the requesting page, bounded by a 20-second timeout. A
  half-initialised approval record would be exactly the state the settle
  interlock exists to keep out of that record, and the same estimate
  previously failed after the user had typed their password.
- verifySignedTx compares the artifact field by field over
  SERIALIZED_FIELDS[type], plus the type itself. A quantity the approval
  does not fix is a refusal rather than a skipped comparison. The ceilings
  stay as a documented backstop and now also apply at population, where they
  bound what an RPC node can talk the wallet into displaying.
- The approval pins the address it was raised for. Verification uses that
  address, not getActiveAddress(), and an address switch between approval and
  signing refuses rather than signing from an account the screen never named
  — including a switch during population, and on the message-signing path. A
  request naming an address that is not the active one is refused outright.
- The approval screen shows the network, gas limit, fee per gas, maximum fee
  and nonce it now vouches for, and the popup signs the object it was given
  with no provider and no population of its own.

The settle chokepoint is untouched: one delete of pendingApprovals and one
approval.resolve(), both inside settleApproval(), the claim taken
synchronously before the first await, and a refused settle still leaving the
approval window standing.
This commit is contained in:
2026-08-12 09:47:25 +00:00
parent a08ba6a66d
commit 993afbc5da
11 changed files with 1808 additions and 346 deletions

View File

@@ -311,10 +311,15 @@ pages. When a site requests access to your wallet:
time.
When a connected site requests a transaction, a separate approval popup appears
showing the transaction details (from, to, value, data). You must enter your
password and click "Confirm" to authorize it. Message and typed-data signature
requests work the same way, with a "Sign" button, and also require your
password.
showing the transaction details (from, to, value, data, network fee, network and
nonce). Every one of those values is checked against the transaction that is
actually signed before anything is broadcast, so what you read on that screen is
what goes out or nothing does. The popup appears once the wallet has worked out
the fee and gas from the network, which takes a moment; if that fails, no popup
appears and the site is told the transaction could not be prepared. You must
enter your password and click "Confirm" to authorize it. Message and typed-data
signature requests work the same way, with a "Sign" button, and also require
your password.
If the requesting site's domain is on the phishing blocklist, all three approval
screens show a red phishing warning before you decide.