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.
In
verifyXPaymentAuththevalidBeforeexpiry check is skipped when a caller omitsvalidBeforefrom 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 shortvalidBeforewindow as the replay mitigation.Where (
src/lib/middleware/predicate-gate.ts)from/to/signature/nonce;validBeforeis optional and the header is a rawJSON.parse(L507) with no schema.if (auth.validBefore !== undefined) { ...expiry... }skips the expiry check entirely whenvalidBeforeis absent.validBefore: BigInt(auth.validBefore ?? "0")recovers the signature withvalidBefore = 0, so a token the caller signed withvalidBefore=0verifies cleanly.Net: a caller can sign an EIP-3009
TransferWithAuthorizationwithvalidBefore=0, omitvalidBeforefrom the X-Payment header, and obtain a never-expiring authorization.Inconsistency
verifyEip3009Auth(same file) has no?? "0"fallback at signature recovery (L458BigInt(authorization.validBefore)), so a missingvalidBeforethere throws and returns 401. The two paths disagree on whether a missingvalidBeforeis acceptable.Why it matters
docs/predicate-gating-guide.mddocuments the model: "the gate does not track nonces server-side, so callers should keepvalidBeforeshort to limit the replay window." The shortvalidBeforewindow is the replay mitigation; an authorization with novalidBeforeremoves 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 (
frommust equal the recovered signer;tomust 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
validBeforeinverifyXPaymentAuth(treat absent as invalid rather than defaulting to 0), mirroringverifyEip3009Auth, so the documented short-window mitigation always holds.