Hi, I noticed a possible optional-verifier issue in the current payment gate helpers.
In src/server/x402-gate.ts, the gate returns a challenge when no X-PAYMENT header exists:
72: export function createX402Gate(config: X402GateConfig) {
82: // Check for existing X-PAYMENT header
83: const paymentHeader =
84: (req.headers['x-payment'] as string) ||
85: (req.headers['X-PAYMENT'] as string);
87: if (!paymentHeader) {
91: const challenge = {
98: accepts: [
101: network: config.network,
102: amount: config.amount,
103: payTo: config.payTo,
104: asset: config.asset,
When a header is present, the gate decodes it and performs some local field checks:
114: // Decode X-PAYMENT proof
116: const decoded = Buffer.from(paymentHeader, 'base64').toString('utf-8');
117: const proof: X402Proof = JSON.parse(decoded);
119: // Basic validation
120: if (!proof.payload?.transaction) {
127: // Verify payment destination matches
128: if (proof.accepted?.payTo && proof.accepted.payTo !== config.payTo) {
135: // Verify amount matches
136: if (proof.accepted?.amount && proof.accepted.amount !== config.amount) {
The actual payment verification hook is optional. If config.verifyPayment is not supplied, the middleware attaches the decoded proof and calls the protected handler:
143: // Custom verification (if provided)
144: if (config.verifyPayment) {
145: const isValid = await config.verifyPayment(proof);
146: if (!isValid) {
148: res.writeHead(402, { 'Content-Type': 'application/json' });
149: res.end(JSON.stringify({ error: 'Payment verification failed' }));
150: return;
151: }
152: }
154: // Attach proof to request for downstream handlers
155: req.payment = proof;
156: log(`[x402-gate] Payment verified: tx=${proof.payload.transaction.slice(0, 16)}...`);
157: next();
The MPP gate has the same shape: a valid-looking credential plus optional verifier is enough to reach next():
123: // Decode Payment credential
125: const credentialBase64 = authHeader.slice('Payment '.length);
130: const decoded = Buffer.from(padded, 'base64').toString('utf-8');
131: const credential: MppCredential = JSON.parse(decoded);
133: // Validate credential structure
134: if (!credential.challenge?.id || !credential.payload?.transaction) {
141: // Check realm matches
142: if (credential.challenge.realm !== config.realm) {
149: // Check expiration
160: // Custom verification
161: if (config.verifyCredential) {
162: const isValid = await config.verifyCredential(credential, credential.challenge.id);
170: // Attach credential to request
171: req.mppPayment = credential;
173: log(`[mpp-gate] Payment verified: tx=${credential.payload.transaction.slice(0, 16)}...`);
174: next();
The combined gate dispatches to either protocol based only on header presence:
52: // Detect which protocol the client is using
53: const hasX402 = !!(req.headers['x-payment'] || req.headers['X-PAYMENT']);
54: const hasAuth = !!(
55: req.headers['authorization'] &&
56: (req.headers['authorization'] as string).startsWith('Payment ')
59: if (hasX402) {
61: return x402Middleware(req, res, next);
64: if (hasAuth) {
66: return mppMiddleware(req, res, next);
The risky path is:
source: X-PAYMENT or Authorization: Payment header
transform: base64/JSON decode and local shape checks
sink: req.payment/req.mppPayment assignment and next()
Field checks on client-provided accepted or credential data are useful, but they are not the same as verifier-backed authorization, settlement, replay protection, or transaction lookup. If users instantiate the gate without verifyPayment or verifyCredential, a forged proof with matching local fields may be enough to enter the protected handler.
Possible hardening directions:
- Make
verifyPayment / verifyCredential required for production gates.
- Provide an explicit
unsafeAllowUnverifiedProofsForDevelopment option if shallow validation is needed for tests.
- Fail closed when no verifier is configured, unless the caller opts into mock mode.
- Add tests that instantiate the gates without verifier callbacks and pass forged matching headers; decide whether those should be rejected by default.
I am reporting this as a potential issue rather than a confirmed exploit, since the library may expect callers to always provide verifier callbacks in real deployments.
Hi, I noticed a possible optional-verifier issue in the current payment gate helpers.
In
src/server/x402-gate.ts, the gate returns a challenge when noX-PAYMENTheader exists:When a header is present, the gate decodes it and performs some local field checks:
The actual payment verification hook is optional. If
config.verifyPaymentis not supplied, the middleware attaches the decoded proof and calls the protected handler:The MPP gate has the same shape: a valid-looking credential plus optional verifier is enough to reach
next():The combined gate dispatches to either protocol based only on header presence:
The risky path is:
Field checks on client-provided
acceptedor credential data are useful, but they are not the same as verifier-backed authorization, settlement, replay protection, or transaction lookup. If users instantiate the gate withoutverifyPaymentorverifyCredential, a forged proof with matching local fields may be enough to enter the protected handler.Possible hardening directions:
verifyPayment/verifyCredentialrequired for production gates.unsafeAllowUnverifiedProofsForDevelopmentoption if shallow validation is needed for tests.I am reporting this as a potential issue rather than a confirmed exploit, since the library may expect callers to always provide verifier callbacks in real deployments.