Skip to content

Potential paywall matching issue: mounted paths may miss configured endpoint keys #49

@chenshj73

Description

@chenshj73

Hi, I noticed a possible paywall route matching issue in the gateway.

In src/gateway/server.ts, the x402 server is configured with full endpoint keys:

386:     x402 = createX402Server(
387:       {
388:         payToAddress: config.x402.server.payToAddress,
389:         network: config.x402.server.network || 'solana',
390:         facilitatorUrl: config.x402.facilitatorUrl,
391:       },
392:       {
393:         'POST /api/compute': { priceUsd: 0.01, description: 'Compute request' },
394:         'POST /api/backtest': { priceUsd: 0.05, description: 'Strategy backtest' },
395:         'GET /api/features': { priceUsd: 0.002, description: 'Feature snapshot' },
396:         'POST /api/launch/token': { priceUsd: 1.00, description: 'Token launch' },
397:         'POST /api/launch/swap': { priceUsd: 0.10, description: 'Bonding curve swap' },
398:         'POST /api/launch/claim-fees': { priceUsd: 0.10, description: 'Claim creator fees' },
399:       }
400:     );
403:     // Apply x402 middleware to premium routes
404:     app.use(['/api/compute', '/api/backtest', '/api/features', '/api/launch/token', '/api/launch/swap', '/api/launch/claim-fees'], x402.middleware);

In src/payments/x402/index.ts, the middleware derives the endpoint key from req.path:

569:     async middleware(req: any, res: any, next: () => void) {
570:       // Get endpoint path
571:       const path = req.path || req.url?.split('?')[0] || '';
572:       const method = req.method || 'GET';
573:       const endpointKey = `${method} ${path}`;
576:       const endpointConfig = endpoints[endpointKey] || endpoints[path];
577:       if (!endpointConfig) {
578:         return next();
579:       }

Because this middleware is mounted at paths such as /api/launch/token, Express may expose the mounted remainder as req.path rather than the full original URL. If path is / or otherwise stripped, it will not match keys like POST /api/launch/token, and the code goes directly to next().

When verification does run, the local accounting uses the configured endpoint price, but I did not see a local comparison between the submitted payment payload and the current endpoint's amount, recipient, network, or resource:

623:         // Verify with facilitator
624:         const verified = await verifyPayment(payload);
626:         if (!verified) {
627:           res.status?.(402) || (res.statusCode = 402);
...
633:         stats.totalPayments++;
634:         stats.totalRevenue += endpointConfig.priceUsd;
647:           { path, amount: endpointConfig.priceUsd, payer: payload.payer },
651:         // Proceed to handler
652:         next();

The protected launch routes perform high-impact actions. For example, src/gateway/launch-routes.ts launches a token:

492:   router.post('/token', async (req: Request, res: Response) => {
519:     const validation = validateLaunchRequest(req.body);
527:     try {
528:       // Step 1: Resolve metadata URI
532:         uri = await uploadMetadata({
574:       // Step 3: Launch token
581:         const r = await createDbcPoolWithFirstBuy(connection, keypair, {
590:         const { createDbcPool } = await import('../solana/meteora-dbc.js');
644:       res.json({ ok: true, data: { ...response, launchId: record.id } });

And the swap route uses the server keypair to execute a swap:

735:   router.post('/swap', async (req: Request, res: Response) => {
768:       // Get quote first to calculate minimumAmountOut with slippage
769:       const { getDbcSwapQuote, swapOnDbcPool } = await import('../solana/meteora-dbc.js');
780:       const result = await swapOnDbcPool(connection, keypair, {
787:       res.json({
790:           signature: result.signature,

The effective risk is:

full endpoint keys configured -> mounted middleware computes stripped path -> no endpointConfig -> next() -> premium handler

A safer design would use req.originalUrl or req.baseUrl + req.path for endpoint matching, add tests for mounted middleware paths, and locally bind verified payments to the current endpoint price, recipient, network, and resource before calling next(). I am reporting this as a potential issue rather than a confirmed exploit, since exact Express path behavior should be confirmed in the deployed version, but the current source has a clear route-key mismatch risk.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions