Displayed transaction summary is not derived from the signed payload
packages/sdk-core/src/sign/request.ts:L64–L118
- Likelihood
- Medium
- Impact
- High
- Weakness
- CWE-451
- Description
buildSignRequestaccepts a transaction and an optionaldisplayobject. Whendisplayis present, its recipient, amount and memo are used for the human-readable summary sent to the wallet; theSignDocthat will actually be signed is serialised separately from the transaction argument.Nothing checks that the two agree. A page that controls both arguments can show the user "Send 1 ATOM to cosmos1…abc" while the
SignDoccontains aMsgSendof the full balance to a different address, or an authzMsgGrant.Wallets that trust the SDK summary, which is the intended integration pattern, display the false summary next to the approval button.
- Preconditions
- The user connects a wallet to a malicious or compromised page
- The wallet displays the SDK-provided summary
- Exploit scenario
- 1.A compromised dApp page calls buildSignRequest with a MsgGrant giving the attacker authz over the user’s funds.
- 2.It passes display = { recipient: <the expected address>, amount: "1 ATOM" }.
- 3.The wallet shows the small transfer; the user approves.
- 4.The signed MsgGrant lets the attacker execute transfers on the user’s behalf.
- Impact
Users can be induced to sign transactions materially different from what they were shown, up to loss of all funds controlled by the key.
- Recommendation
Remove the
displayoverride. Generate the summary by decoding the exact bytes of theSignDocthat will be signed, render unknown message types explicitly as unrecognised, and add differential tests that decode each request independently.- Fix
- packages/sdk-core/src/sign/request.tsdiff
-export function buildSignRequest(tx: TxInput, display?: DisplayHints): SignRequest {- const signDoc = serializeSignDoc(tx)- const summary = display ? summaryFromHints(display) : summaryFromTx(tx)+export function buildSignRequest(tx: TxInput): SignRequest {+ const signDoc = serializeSignDoc(tx)+ // The summary is always decoded from the bytes that will be signed.+ const summary = summaryFromSignDoc(decodeSignDoc(signDoc.bytes)) return { signDoc, summary } } - Resolution
Display hints removed from the public API; the summary is decoded from SignDoc bytes. Unknown message types are rendered as "Unrecognised message" with the raw type URL. Differential tests (DT-01) added.
