Skip to content

predicate gate: X-Payment validBefore expiry check is skippable by omitting the field (inconsistent with the EIP-3009 path) #9

Description

@Nexory

In verifyXPaymentAuth the validBefore expiry check is skipped when a caller omits validBefore from the authorization, producing an authorization that never expires. This is inconsistent with the sibling EIP-3009 path and with the documented gating model, which relies on a short validBefore window as the replay mitigation.

Where (src/lib/middleware/predicate-gate.ts)

  • L525: the required-fields check requires only from/to/signature/nonce; validBefore is optional and the header is a raw JSON.parse (L507) with no schema.
  • L560: if (auth.validBefore !== undefined) { ...expiry... } skips the expiry check entirely when validBefore is absent.
  • L598: validBefore: BigInt(auth.validBefore ?? "0") recovers the signature with validBefore = 0, so a token the caller signed with validBefore=0 verifies cleanly.

Net: a caller can sign an EIP-3009 TransferWithAuthorization with validBefore=0, omit validBefore from the X-Payment header, and obtain a never-expiring authorization.

Inconsistency

verifyEip3009Auth (same file) has no ?? "0" fallback at signature recovery (L458 BigInt(authorization.validBefore)), so a missing validBefore there throws and returns 401. The two paths disagree on whether a missing validBefore is acceptable.

Why it matters

docs/predicate-gating-guide.md documents the model: "the gate does not track nonces server-side, so callers should keep validBefore short to limit the replay window." The short validBefore window is the replay mitigation; an authorization with no validBefore removes that window, so a captured token (the documented bounded-replay scenario) becomes replayable indefinitely.

Impact (low)

The token only authenticates the signer's own address (from must equal the recovered signer; to must equal the operator), and the on-chain predicate (tryHasAccess) is re-evaluated per request, so this does not enable impersonation of other addresses or bypass of a revoked predicate. The concrete effect is that the documented short-replay-window guarantee can be eliminated by the caller. Filing as a correctness/hardening issue, not a high-severity bypass.

Suggested fix

Reject a missing validBefore in verifyXPaymentAuth (treat absent as invalid rather than defaulting to 0), mirroring verifyEip3009Auth, so the documented short-window mitigation always holds.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions