Skip to content
WebCureby Swasti
Critical

Fake payment webhooks

Anyone can POST “payment succeeded” to your webhook and unlock a paid plan.

What it is

Payment providers notify your app through webhooks. If you don't verify the signature, anyone who finds the URL can send a fake “payment captured” event.

Why fast-built apps get it

Verifying needs the raw request body and a secret, which trips up generated code. Skipping it “works” in testing.

Before and after the cure

app/api/webhooks/razorpay/route.tsVulnerable
export async function POST(req: Request) {
  const event = await req.json();           // trusted blindly
  if (event.event === "payment.captured") {
    await activatePlan(event.payload.payment.entity.notes.userId);
  }
  return new Response("ok");
}
app/api/webhooks/razorpay/route.tsCured
export async function POST(req: Request) {
  const body = await req.text();
  const signature = req.headers.get("x-razorpay-signature") ?? "";
  if (!verifySignature(body, signature, process.env.RAZORPAY_WEBHOOK_SECRET!)) {
    return new Response("Invalid signature", { status: 400 });
  }
  const event = JSON.parse(body);
  await processOnce(event.id, () => activateFromVerifiedPayment(event));
  return new Response("ok");
}

What it can cost you

  • Free upgrades to paid plans
  • Orders marked paid that never were
  • Credits or subscriptions granted to attackers

Check your own app

  • Find your Stripe or Razorpay webhook route; does it verify a signature header?
  • Check you look up the payment with the provider before trusting the event
  • Make sure each event is processed only once

The cure

  • Verify the webhook signature on the raw body
  • Process each event ID once (idempotency)
  • Grant access from verified payment data, not from client-supplied notes

Does your app have this one?

We'll check this and every other loophole on your app, then give you the cure.

Talk to us on WhatsApp