Skip to content

fix: bind attribution memo nonce to challenge ID#291

Merged
brendanjryan merged 1 commit intomainfrom
fix/veria-123-challenge-bound-memo
Apr 3, 2026
Merged

fix: bind attribution memo nonce to challenge ID#291
brendanjryan merged 1 commit intomainfrom
fix/veria-123-challenge-bound-memo

Conversation

@brendanjryan
Copy link
Copy Markdown
Collaborator

@brendanjryan brendanjryan commented Apr 2, 2026

Summary

Fixes transaction hash stealing vulnerability in push-mode (hash credential) payments, with defense-in-depth for transaction credentials.

In a very rare occurrence attackers could steal legitimate on-chain transaction hashes and submit them against a different challenge, receiving services for free while locking out the legitimate payer.

Breaking changes

  • Attribution.encode() now requires challengeId — old callers without it will get a type error
  • Old clients that generate random attribution nonces or plain transfers will be rejected by new servers in push mode

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 2, 2026

Open in StackBlitz

npm i https://pkg.pr.new/mppx@291

commit: de0c99e

@brendanjryan brendanjryan force-pushed the fix/veria-123-challenge-bound-memo branch from ba33352 to 1af4502 Compare April 2, 2026 23:10
@brendanjryan brendanjryan changed the title fix: bind attribution memo nonce to challenge ID (VERIA-123) fix: bind attribution memo nonce to challenge ID Apr 2, 2026
@brendanjryan brendanjryan force-pushed the fix/veria-123-challenge-bound-memo branch 2 times, most recently from 9cc453d to 9a3238c Compare April 2, 2026 23:23
Copy link
Copy Markdown

@tempoxyz-bot tempoxyz-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👁️ Cyclops Review

Note: Head drift detected — audited commit 1af4502 differs from current head 9a3238c. Findings have been re-checked against the current diff. Earlier findings about the missing transaction-path check, the custom memo regression, and the missing server fingerprint verification are resolved on the current head and omitted from inline comments.

Surviving Findings

⚠️ [ISSUE] Receipt-global memo scan allows unrelated TransferWithMemo log to satisfy challenge bindingassertChallengeBoundMemo scans all memo logs in the receipt rather than verifying the nonce on the specific log(s) that satisfied assertTransferLogs. A multi-call transaction with a same-currency dust transfer carrying a different challenge's nonce can satisfy both challenges independently if their hash stores aren't shared. See inline comment.

🛡️ [DEFENSE-IN-DEPTH] Deterministic challenge.id weakens memo-based replay protection (src/Challenge.ts:612-625, not in this diff) — challenge.id is HMAC(secret, realm|method|intent|request|expires|...) with no per-issuance randomness. If two 402 responses share the same expires (e.g., issued within the same millisecond), they produce identical IDs, and the memo nonce binding provides no replay resistance between them. Consider injecting a random value into opaque to ensure per-issuance uniqueness.

Reviewer Callouts
  • Receipt/log binding: The new defense proves some memo in the receipt is challenge-bound, but not that the matched payment log carries that memo. Evaluate whether the dust-transfer vector is practical in production.
  • Store topology: The exploit is most practical when hash stores are isolated (Store.memory() per handler). Deployment guidance should recommend shared stores for routes accepting the same payment parameters.
  • Challenge ID uniqueness: The deterministic challenge ID predates this PR. If challenge.id is now a security nonce, adding per-issuance randomness is worth a follow-up.

Stale Findings (fixed on current head)

  • Transaction replay bypass via transaction payloadsassertChallengeBoundMemo now called in both paths.
  • Custom memo regressionif (!memo) guard preserves custom memo support.
  • Missing server fingerprint checkverifyServer() now called before accepting a memo log.

})

const bound = memoLogs.some((log) => {
if (!TempoAddress.isEqual(log.address, parameters.currency)) return false
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [ISSUE] Receipt-global memo scan allows unrelated TransferWithMemo log to satisfy challenge binding

This memoLogs.some(...) iterates over ALL TransferWithMemo logs in the receipt, accepting any log whose token matches currency and whose nonce matches challengeId. It does not verify that the accepted memo log is the same log that satisfied assertTransferLogs.

A single multi-call transaction can contain: (1) the real merchant payment for challenge A with A's nonce, and (2) a same-currency dust transfer to an arbitrary address carrying challenge B's nonce. If the two challenges' hash stores are not shared (e.g., separate Store.memory() per handler), the same tx hash satisfies both challenges.

Recommended Fix: Return the matched log(s) from assertTransferLogs and require assertChallengeBoundMemo to verify the nonce on those exact logs rather than scanning the entire receipt.

@brendanjryan brendanjryan force-pushed the fix/veria-123-challenge-bound-memo branch 6 times, most recently from c9d8541 to 5cec129 Compare April 3, 2026 02:25
Prevents transaction hash stealing by deriving the 7-byte memo nonce
from keccak256(challengeId)[0..6] instead of random bytes. The server
verifies this binding for both hash and transaction credentials,
rejecting payments whose memo nonce does not match the challenge.

- Attribution.encode() requires challengeId (no random fallback)
- Client Charge passes challenge.id when encoding attribution memo
- Server Charge verifies challenge-bound nonce for hash and transaction
- Server fingerprint also verified in assertChallengeBoundMemo
- Challenge binding skipped when explicit memo is set (already strict-matched)
- New tests: cross-challenge theft, non-MPP memo rejection, split payments
@brendanjryan brendanjryan force-pushed the fix/veria-123-challenge-bound-memo branch from 5cec129 to de0c99e Compare April 3, 2026 02:28
@brendanjryan brendanjryan merged commit e7147c2 into main Apr 3, 2026
8 checks passed
@brendanjryan brendanjryan deleted the fix/veria-123-challenge-bound-memo branch April 3, 2026 02:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants