Hi, I noticed a possible payment signature reuse issue in the x402 fetch middleware.
The tool metadata includes payment-specific fields such as scheme, network, amount, asset, and payee:
48: interface ToolPaymentMeta {
49: paymentRequired: boolean;
50: accepts?: PaymentRequiredAccept[];
51: scheme?: string;
52: network?: string;
53: amount?: string;
54: asset?: string;
55: payTo?: string;
56: maxTimeoutSeconds?: number;
When handling a paid tools/call, the middleware checks the tool metadata:
187: // Parse the request body to find tools/call requests
188: const toolName = extractToolCallName(init.body);
193: // Look up tool metadata
194: const tool = getToolByName(toolName);
200: // Check _meta.x402
201: const meta = (tool as { _meta?: { x402?: ToolPaymentMeta } })._meta;
202: const x402 = meta?.x402;
203: if (!x402 || !x402.paymentRequired) {
204: return undefined;
205: }
However, it returns a cached signature before selecting a payment requirement for the current tool:
207: // Return cached signature if available
208: if (paymentCache.signature) {
209: logger.debug(`Using cached payment signature for tool "${toolName}"`);
210: return paymentCache.signature;
211: }
213: const accept = selectAcceptFromToolMeta(x402, schemePreference);
221: try {
222: const result = await signPayment({ wallet, accept });
226: paymentCache.signature = result.paymentSignatureBase64;
227: return result.paymentSignatureBase64;
The 402 fallback path also stores a single cached signature:
267: // Sign the payment
268: try {
269: const result = await signPayment({
270: wallet,
271: accept,
272: resource: header.resource,
273: });
279: // Cache the freshly signed payment for subsequent calls
280: paymentCache.signature = result.paymentSignatureBase64;
283: const retryInit = injectPayment(originalInit, result.paymentSignatureBase64);
284: return await baseFetch(url, retryInit);
The tool name extraction supports different tools/call requests in the same session:
303: // Handle single request
304: if (!Array.isArray(parsed)) {
305: const req = parsed as JsonRpcRequest;
306: if (req.method === 'tools/call' && req.params?.name) {
307: return req.params.name;
The effective cache behavior appears to be:
first paid tool signs payment -> paymentCache.signature -> later paid tool reuses cached signature
I did not see the cache key include tool name, resource, amount, payee, network, asset, or scheme. This means a signature created for one paid tool can be injected into a later different paid tool call from the same session.
A strict server may reject the mismatch, but the client-side middleware still sends a stale proof for a different payment context. A safer design would cache by payment requirement identity, or avoid reusing a payment signature across tools/resources unless the server explicitly declares it reusable. I am reporting this as a potential issue rather than a confirmed exploit because server-side validation may fail closed.
Hi, I noticed a possible payment signature reuse issue in the x402 fetch middleware.
The tool metadata includes payment-specific fields such as scheme, network, amount, asset, and payee:
When handling a paid
tools/call, the middleware checks the tool metadata:However, it returns a cached signature before selecting a payment requirement for the current tool:
The 402 fallback path also stores a single cached signature:
The tool name extraction supports different
tools/callrequests in the same session:The effective cache behavior appears to be:
I did not see the cache key include tool name, resource, amount, payee, network, asset, or scheme. This means a signature created for one paid tool can be injected into a later different paid tool call from the same session.
A strict server may reject the mismatch, but the client-side middleware still sends a stale proof for a different payment context. A safer design would cache by payment requirement identity, or avoid reusing a payment signature across tools/resources unless the server explicitly declares it reusable. I am reporting this as a potential issue rather than a confirmed exploit because server-side validation may fail closed.