Skip to content

feat: add maxReceiptAgeSeconds to limit hash-credential historical replay#10

Merged
AlejandroFabianCampos merged 1 commit into
mainfrom
feat/hash-receipt-age-limit
Apr 30, 2026
Merged

feat: add maxReceiptAgeSeconds to limit hash-credential historical replay#10
AlejandroFabianCampos merged 1 commit into
mainfrom
feat/hash-receipt-age-limit

Conversation

@AlejandroFabianCampos
Copy link
Copy Markdown
Collaborator

@AlejandroFabianCampos AlejandroFabianCampos commented Apr 30, 2026

Summary

Addresses the security review's finding on verifyHash: the credential payload binds nothing to the specific challenge, so any historical Transfer to the recipient matching the requested token + amount can be claimed as proof of payment, once each.

This adds an opt-in server config option maxReceiptAgeSeconds. When set, verifyHash fetches the receipt's block and rejects any receipt whose timestamp is older than N seconds at verification time. When unset, behavior is unchanged from today (no age limit).

Why opt-in (no default)

A default would silently change verification semantics for every existing operator on upgrade. Operators on slow chains (or with long retry budgets) might see legitimate payments rejected; operators with sub-second confirmation could already be tighter. Without a sensible universal default, opt-in is the right shape — operators consciously pick a value that matches their chain + UX.

What it does and doesn't fix

  • ✅ Closes the historical-Transfer-replay class (any tx older than the window is rejected).
  • ❌ Does not close concurrent third-party replay (a legitimate customer paying $X to the merchant within the window — attacker grabs that txHash). Mitigation here would require an on-chain commit of `challenge.id`, a much bigger spec extension. Flagged in the README.
  • ✅ Anchors to `Date.now()` instead of optional `challenge.expires` — works regardless of how the challenge was constructed; semantics is the natural "max age at verification time."

Files changed

  • `src/types.ts` — new `maxReceiptAgeSeconds?: number` on `ServerParameters` with full JSDoc.
  • `src/server/Charge.ts` — destructure + thread to `verifyHash`.
  • `src/server/verifiers/hash.ts` — when set, fetch `getBlock` for the receipt and compare `block.timestamp` against `Date.now() - maxReceiptAgeSeconds`.
  • `src/server/verifiers/hash.test.ts` — 4 new tests (current-behavior preserved, fresh accept, old reject, reservation release on age rejection). Stub `getBlock` added to the test harness.
  • `README.md` — `> [!CAUTION]` block under the credentials table calling out the historical-replay class explicitly + new row in the `evm.charge` server config table.

Test plan

  • `npm run verify` (lint + typecheck + 95 tests pass, 9 skipped — was 91)
  • Set `maxReceiptAgeSeconds: 600` on a deployed server, replay a years-old historical Transfer, confirm rejection
  • Same server, fresh hash payment within the window, confirm acceptance

Note

Medium Risk
Adds an opt-in age check to hash receipt verification, which can reject previously-accepted payments if misconfigured and impacts a security-sensitive payment-verification path.

Overview
Adds an opt-in server configuration maxReceiptAgeSeconds that, when set, makes verifyHash fetch the receipt block timestamp and reject hash credentials older than the configured window.

Threads the new option through evm.charge server parameters, updates README configuration/docs with a caution about hash’s weak binding, and expands hash verifier tests to cover accept/reject behavior and reservation release on age-based rejection.

Reviewed by Cursor Bugbot for commit 1839ccb. Bugbot is set up for automated code reviews on this repo. Configure here.

@AlejandroFabianCampos AlejandroFabianCampos merged commit edb846a into main Apr 30, 2026
2 checks passed
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