Skip to content

Next.js x402 example grants access when any X-PAYMENT header is present #81

Description

@chenshj73

Hi Nirium team,

I found a payment-gating defect in the older examples/nextjs-x402 sample. The wrapper treats the mere presence of X-PAYMENT as enough to call the paid handler, without verifying or settling the x402 proof first.

Affected code:

// examples/nextjs-x402/app/lib/nirium-x402-seller.ts
export function withNiriumX402(
  config: X402SellerConfig,
  handler: PaidRouteHandler,
) {
  return async function paidRoute(request: Request): Promise<Response> {
    const paymentHeader = request.headers.get("x-payment");

    if (!paymentHeader) {
      return NextResponse.json(
        {
          x402Version: 1,
          accepts: [
            {
              scheme: "exact",
              network: config.network,
              asset: "USDC",
              payTo: config.payTo,
              maxAmountRequired: config.priceUsdc,
              resource: config.resource,
              description: config.description,
            },
          ],
          error: "Payment required",
        },
        {
          status: 402,
          headers: {
            "Cache-Control": "no-store",
            "X-Accept-Payment": "x402",
          },
        },
      );
    }

    return handler(request, paymentHeader);
  };
}

That wrapper protects the premium route here:

// examples/nextjs-x402/app/api/premium/signals/route.ts
export const GET = withNiriumX402(
  {
    network: process.env.NIRIUM_X402_NETWORK ?? "stellar:testnet",
    priceUsdc: process.env.NIRIUM_X402_PRICE_USDC ?? "0.02",
    payTo: process.env.NIRIUM_X402_PAY_TO ?? "",
    resource: "/api/premium/signals",
    description: "Nirium premium market signals",
  },
  async () => {
    const { signals } = await agent.getRecentSignals(5);

    return Response.json({
      ok: true,
      count: signals.length,
      signals,
    });
  },
);

As written, any non-empty X-PAYMENT value reaches the paid handler and returns the premium signal data. There is no parse, signature verification, facilitator /verify, facilitator /settle, payer/resource/amount binding check, or settlement receipt check before the handler runs.

The README does warn that production should connect the wrapper to a facilitator before trusting the header. The reason I am filing this anyway is that the example is named and documented as a paid x402 App Router handler, and the expected flow says that retrying with an X-PAYMENT proof returns the paid JSON. A user copying this example as a starting point could accidentally ship a route where X-PAYMENT: anything is enough to unlock the resource.

Suggested fix:

  • Replace withNiriumX402 with the same real x402Serve()/facilitator-backed path used by examples/deploy-x402-vercel/lib/x402-handler.ts, or make this example fail closed unless a verifier/settlement callback is configured.
  • Add a smoke test that sends an arbitrary non-empty X-PAYMENT header and asserts the paid handler is not called.
  • Consider changing the header name in this sample to the current protocol header used elsewhere in the repo (PAYMENT-SIGNATURE / payment-signature) so readers do not copy an outdated interface.

I reviewed this against commit 6764804790c4dfda706605d53aa4fcc136752265.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardThird CampaignCampaign: Third Campaign

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions