Skip to content
Quantix Security Labs

Authorisation phishing through a signing SDK

A look-alike dApp asked Cosmos users to sign authz grants. How the campaign was detected and contained in hours, and how a later assessment found the general cause: approval screens built from what the requester said, not what was signed.
HighSignature phishingIncident response · 9 min read
Illustration: a wallet signs for a look-alike dApp front beside the real one, and the grant it carried runs into a drainer funnel.

The case at a glance

Larkspur Wallet SDK

Cosmos · Wallet InfrastructureSecurity profileResearch case · Quantix Protocol Research

During the case
BB73.2
Current profile
A81.4
To containment
2 h 10 min
To verified fix
114 days
  1. 03Problem

    On the morning of 18 May 2026 the authorisation-concentration rule fired: 37 accounts had granted unlimited MsgSend authority, with no expiry, to the same unknown grantee within an hour.

    Read section
  2. 06Vulnerability

    HighSignature phishing

    The approval screen showed a description of the transaction supplied by the party requesting the signature, not a decoding of the transaction itself.

    Read section
  3. 08Remediation

    1. 01Flag the grantee in the registry with a warn-and-block policy
    2. 02Decode authz grants and require an expiry (SDK 2.4)
    3. 03Publish revocation guidance for standing grants

    and 2 more steps

    Read section
  4. 09Outcome

    New grants to the grantee were blocked from the morning of the incident, and revocations ran at twice the usual rate after the advisory.

    Risk Index BB → A, +8.2 points

    Read section

Background

The Larkspur Wallet SDK connects Cosmos dApps to wallets. A dApp builds a sign request with the SDK; the pairing relay forwards it to the wallet, which shows a summary and returns a signature over a SignDoc.

Cosmos SDK chains include the authz module. A MsgGrant with GenericAuthorization for /cosmos.bank.v1beta1.MsgSend lets the grantee send the granter’s tokens with MsgExec until the grant expires or is revoked — and a grant without an expiry never expires.

Architecture

The Signing SDK builds sign requests and their human-readable summaries; the Pairing relay pairs a dApp session with a wallet and forwards encrypted messages; the Chain registry supplies chain metadata and, after this incident, flagged addresses.

Figure 1Larkspur Wallet SDK: components and paths involved in this case

Larkspur Wallet SDK: architecture relevant to this case6 elements and 6 connections. Highlighted components are directly involved in the case; dashed red arrows are attack paths, dotted brass arrows are control paths.sign requestsession_proposeencrypted requestsummarybroadcast MsgGrantflagged addressesLook-alike dAppcloned integrator metadataSigning SDKBuilds sign requests (Aminoand Direct) and verifiesresponsesPairing relaySession pairing andencrypted messageforwardingWallet approval screenshows SDK summaryChain registryServes chain IDs, RPCendpoints and feedenominationsCosmos chain (authz)MsgGrant / MsgExec
Protocol componentInvolved in this caseExternal partyAttack pathControl path

Elements:

  • Look-alike dApp (adversary): cloned integrator metadata
  • Signing SDK (protocol component, involved in this case): Builds sign requests (Amino and Direct) and verifies responses
  • Pairing relay (protocol component, involved in this case): Session pairing and encrypted message forwarding
  • Wallet approval screen (external party): shows SDK summary
  • Chain registry (protocol component): Serves chain IDs, RPC endpoints and fee denominations
  • Cosmos chain (authz) (external party): MsgGrant / MsgExec

Connections:

  • Look-alike dApp to Signing SDK: sign request (attack path)
  • Look-alike dApp to Pairing relay: session_propose (attack path)
  • Pairing relay to Wallet approval screen: encrypted request (data or call flow)
  • Signing SDK to Wallet approval screen: summary (data or call flow)
  • Wallet approval screen to Cosmos chain (authz): broadcast MsgGrant (data or call flow)
  • Chain registry to Wallet approval screen: flagged addresses (control path)

Components in the Larkspur Wallet SDK profile

  • Signing SDK(involved in this case)

    Builds sign requests (Amino and Direct) and verifies responses

  • Pairing relay(involved in this case)

    Session pairing and encrypted message forwarding

  • Chain registry

    Serves chain IDs, RPC endpoints and fee denominations

  • Staging cloud

    Build pipeline and relay hosting

Security challenge

On the morning of 18 May 2026 the authorisation-concentration rule fired: 37 accounts had granted unlimited MsgSend authority, with no expiry, to the same unknown grantee within an hour. Twenty minutes later the grantee began executing transfers under those grants.

Users reported that the wallet had asked them to approve a session for a dApp they recognised. The response had to stop new grants, protect accounts that had already granted, and explain how an open-ended grant could be approved under that description.

Attack surface

The investigation compared what users saw with what they signed, at each step of the signing path.

Attack surface examined in Larkspur Wallet SDK
SurfaceExposureControl
Approval summaryBuilt from caller-supplied display hints, not from the SignDoc (LW-01)Summary decoded from the bytes being signed
Pairing originPairing not bound to the requesting origin; short identifiers (LW-02)Origin-bound, higher-entropy pairing identifiers
Authz grantsNo special handling; grants without expiry acceptedDecoded grant review; expiry required (SDK 2.4)
Known granteesNo way to warn about a flagged addressRegistry flag with warn-and-block policy
Standing grantsUsers unaware of grants they had madeAdvisory and revocation guidance

Technical investigation

Every signed transaction in the cluster contained a single MsgGrant with GenericAuthorization for MsgSend and no expiration. None contained a transfer. The sessions came from a look-alike domain presenting the name and icon of a legitimate integrator, which the relay could not distinguish from the real one.

The approval screen showed the message type URL, /cosmos.authz.v1beta1.MsgGrant, under a description supplied by the page: “Approve session”. Nothing on the screen said what the grant allowed or that it never expired, and to most users the type URL meant nothing.

Containment came from the registry: a flagged-address update made SDK clients block grants to the grantee two hours after the first alert, and SDK 2.4 shipped the next day with decoded grant review and a mandatory expiry.

The penetration test commissioned afterwards found the general cause behind the page-supplied description. The SDK accepted display hints alongside the transaction and, when present, used them as the summary, while the SignDoc was serialised separately; nothing compared the two. A page could show a one-unit transfer while the user signed a grant (LW-01). The relay’s missing origin binding was reported as LW-02.

Figure 2What the user saw versus what they signed

What the user saw versus what they signedLook-alike dAppWallet (SDK ≤ 2.3)UserChainGrantee1. tx: MsgGrant · description: “Approve session”2. type URL under “Approve session”scope and expiry not shown3. approve4. MsgGrant(GenericAuthorization: MsgSend, no expiry)5. MsgExec[MsgSend(balance)]6. R-12 / R-18 alerts
Ordinary stepAttack stepDefensive step
  1. Look-alike dApp (adversary) to Wallet (SDK ≤ 2.3): tx: MsgGrant · description: “Approve session” (attack step)
  2. Wallet (SDK ≤ 2.3) (on-chain) to User: type URL under “Approve session”. scope and expiry not shown (attack step)
  3. User (participant) to Wallet (SDK ≤ 2.3): approve (normal step)
  4. Wallet (SDK ≤ 2.3) (on-chain) to Chain: MsgGrant(GenericAuthorization: MsgSend, no expiry) (normal step)
  5. Grantee (adversary) to Chain: MsgExec[MsgSend(balance)] (attack step)
  6. Chain (on-chain) to Grantee: R-12 / R-18 alerts (defensive step)
Monitoring events recorded during the incident, with the rule that matched and its threshold
Time (UTC)RuleObservationMeasuredAlert
2026-05-18 08:20:14R-12Authorisation grant concentration37 send authorisations granted to one grantee in one hourHigh37 (threshold 25 grants to one grantee / 1 h)Alert
2026-05-18 08:41:56R-18Delegated execution burstGrantee executed 22 transfers under authz in 40 minutesHigh22 (threshold 10 executions / 1 h)Alert
2026-05-18 09:05:30R-19Session origin mismatchPairing requests with cloned dApp metadataHighCondition matchedAlert
2026-05-18 10:30:00R-09Protocol parameter changeRegistry flags grantee; SDK warns on authz grantsMediumRecorded for the timelineLogged
2026-05-19 14:00:00R-09Protocol parameter changeSDK 2.4 released with decoded grant reviewLowRecorded for the timelineLogged
2026-05-20 09:00:00R-16Activity above baselineGrant revocations at 2.1× the usual rateLow2.1 (threshold 3 × 30-day median)Logged

Timeline

  1. Detection

    37 grants to one grantee in an hour; R-12 alert

  2. Exploitation

    Grantee executes 22 transfers under authz

  3. Pairing requests with cloned dApp metadata identified

  4. Containment

    Registry flags the grantee; SDK clients block grants to it

  5. Fix

    SDK 2.4 released with decoded grant review

  6. Grant revocations at 2.1× the usual rate

  7. Penetration test of SDK and relay begins

  8. Verification

    LW-01 and LW-02 fixes re-tested; report published

Root cause

The approval screen showed a description of the transaction supplied by the party requesting the signature, not a decoding of the transaction itself. Grants were not translated into what they allowed, and the SDK let requester-supplied hints stand in for the summary. Any signing flow that renders requester-supplied metadata instead of decoding the signed bytes can be made to display one action and sign another.

Class
Signature phishing
Severity
High
Components involved
Signing SDKPairing relay
Findings
LW-01LW-02
packages/sdk-core/src/sign/request.ts (SDK 2.3)ts
export function buildSignRequest(tx: TxInput, display?: DisplayHints): SignRequest {  const signDoc = serializeSignDoc(tx)  const summary = display ? summaryFromHints(display) : summaryFromTx(tx) // hints win  return { signDoc, summary }}

Risk evaluation

In the before state the SDK’s two high-severity and two medium-severity issues are open, and Operational Security is reduced until grant review and address flagging existed.

73.2BBElevated risk
During the caseBB73.2
81.4AModerate risk
Current profileA81.4
CodeSecurityOperationalSecurityGovernanceRiskEconomicSecurityInfrastructureResilienceTransparencyDuring the case, Code Security: 58During the case, Operational Security: 70During the case, Governance Risk: 85During the case, Economic Security: 88During the case, Infrastructure Resilience: 74During the case, Transparency: 84Current profile, Code Security: 80Current profile, Operational Security: 78Current profile, Governance Risk: 85Current profile, Economic Security: 88Current profile, Infrastructure Resilience: 74Current profile, Transparency: 84
During the caseCurrent profile

The score moves from 73.2 (BB) to 81.4 (A), a change of +8.2 points. The largest single contribution is Code Security: restoring it from 58.0 to 80.0 alone moves the score from 73.2 to 79.8.

Basis for the case state: LW-01 to LW-04 open in deployed SDK versions; no grant review or address flagging.
ComponentWeightRangeDuring caseTodayScore points
Code Security30%58.080.0+6.6
Operational Security20%70.078.0+1.6
Governance Risk15%85.085.00.0
Economic Security15%88.088.00.0
Infrastructure Resilience10%74.074.00.0
Transparency10%84.084.00.0
Quantix Risk Index100%73.281.4+8.2
During the caseCurrent profileBasis for the case state: LW-01 to LW-04 open in deployed SDK versions; no grant review or address flagging.

Scores use the six weighted components of the Quantix Risk Index. Open findings reduce Code Security by a fixed penalty per severity; the current profile is the protocol’s entry in Quantix Intelligence.

Mitigation strategy

The response ran in two stages. Within hours the registry flagged the grantee, and SDK 2.4 decoded every MsgGrant into plain language and refused grants without an expiry. After the assessment, display hints were removed from the public API entirely: the summary is always decoded from the exact SignDoc bytes, and unknown message types are shown as unrecognised with their type URL.

The relay now binds pairing to the requesting origin with higher-entropy identifiers (LW-02).

  1. 01Flag the grantee in the registry with a warn-and-block policy
  2. 02Decode authz grants and require an expiry (SDK 2.4)
  3. 03Publish revocation guidance for standing grants
  4. 04Remove display hints; decode every summary from the SignDoc
  5. 05Bind pairing to the requesting origin
packages/sdk-core/src/sign (remediation commit)ts
import { MsgGrant } from 'cosmjs-types/cosmos/authz/v1beta1/tx' export function buildSignRequest(tx: TxInput): SignRequest {  const signDoc = serializeSignDoc(tx)  const decoded = decodeSignDoc(signDoc.bytes)        // the bytes that will be signed  return { signDoc, summary: summaryFromSignDoc(decoded), warnings: decoded.messages.flatMap(grantWarnings) }} function grantWarnings(msg: Any): Warning[] {  if (msg.typeUrl !== '/cosmos.authz.v1beta1.MsgGrant') return []  const { grantee, grant } = MsgGrant.decode(msg.value)  if (!grant?.expiration) throw new SignRequestError('Authorisation grants must have an expiry')  return [{ level: 'high', text: 'Allows ' + grantee + ' to act for this account until ' + formatExpiry(grant.expiration) }]}

Security outcome

New grants to the grantee were blocked from the morning of the incident, and revocations ran at twice the usual rate after the advisory. SDK 2.4 closed the grant-specific path the next day.

The penetration test found and closed the general cause: LW-01 and LW-02 were fixed and re-tested before the report was published.

Detection to containment
2 h 10 min
Detection to first fix
29 h 40 min
Detection to verified fix
114 days
Risk Index change
BB → A (+8.2)

Lessons learned

  1. 01

    Show users what they sign, decoded from the signed bytes, never a description from the requester.

  2. 02

    Delegation primitives such as authz grants deserve their own review screen and an expiry.

  3. 03

    Many grants to one grantee in an hour is a strong, cheap phishing signal.

  4. 04

    An incident fix that closes one path is a reason to look for the general cause.

Have a problem like this one?

Tell us what you are building and where it runs. We will reply with the review plan we would use for your architecture.