harden: approvalVerify does not compare chainId, nonce or fee fields; failed signing leaves an unretryable button #174
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?
Two non-blocking findings from the independent review of PR #171 (issue #157),
recorded so they are not lost. Same file, same review, so grouping them.
1.
verifySignedTxdoes not check the fee and ordering fieldssrc/shared/approvalVerify.jsre-derives the signer from the artifact andcompares
from,to,valueanddataagainst the held approval. It doesnot compare
chainId,nonce,gasLimit, or the fee fields(
maxFeePerGas/maxPriorityFeePerGas/gasPrice).Not currently exploitable.
POPUP_ONLY_TYPESatsrc/background/index.js:663-674restricts who can send the message, and onlya holder of the key can produce an artifact that passes verification at all.
But
to/value/datadeliberately exist to catch a signed transaction thatdoes not match what the user approved, and a hostile or buggy fee field is
precisely the case that set does not cover — an absurd
maxFeePerGasdrainsvalue to the validator without touching
toorvalue.Since the whole point of
approvalVerify.jsis that the background does notblindly broadcast whatever an extension page hands it, the check should be
complete rather than nearly complete.
Requirements
verifySignedTxto comparechainIdand the fee fields against theapproval, with full-sentence rejection messages matching the existing style.
nonceandgasLimitneed thought rather than a blanket equality check:they are produced by
populateTransactionin the popup, not supplied by thedApp, so the approval may legitimately not carry a value to compare against.
Decide per-field whether the correct rule is equality, a sanity bound, or no
check, and record the reasoning in the PR. Do not add a comparison that
fails on a legitimate transaction.
chainIdin particular must be checked against the currently selectednetwork, not merely against whatever the artifact claims — a cross-chain
replay is the failure this prevents.
tests/approvalVerify.test.jswith a tamper case per newly checkedfield, mirroring the existing tamper tests.
2. A failed popup-side signing leaves a button that cannot succeed
src/popup/views/approval.js:644-651re-enables the approve button after apopup-side signing failure, but the background has already deleted the
approval at
src/background/index.js:772. A retry therefore cannot succeed —the user clicks again and nothing can happen.
The reviewer notes this is a pre-existing shape, not a regression introduced by
#171. It is still wrong: per the README Clickable Affordance policy, a control
that looks actionable must do something.
Requirements
in the background so a retry genuinely works, or disable the button and show
a full-sentence explanation that the request must be re-initiated from the
site. The first is better UX; the second is simpler and honest. State the
choice and reasoning in the PR.
retryable by failing in the popup before any message is sent. Match that
behaviour if it is reachable here.
Definition of done
verifySignedTxcompareschainIdand the fee fields;nonceandgasLimitare handled per a documented, justified rule.chainIdis validated against the selected network, not just theartifact.
fix.
existing round-trip test still passes unmodified.
TODO.mdupdated in the same commit.make checkpasses.