Skip to content

Potential payment cache reuse issue: session-level signature is reused across tool calls #247

@chenshj73

Description

@chenshj73

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions