ASAASA Standard
Active Phase 1Production Foundation

Stripe Webhook Safety

Billing Safety · BIL-02, BIL-03, BIL-04 · ADM-15 · Priority: P0

Why It Matters

Stripe webhooks are the primary mechanism for keeping your app in sync with payment state. When webhook signature verification is missing, an attacker can send fake webhook events to your endpoint — triggering false fulfillment, granting unauthorized access, or manipulating subscription state.

An estimated 40% of billing-related issues in AI-built apps stem from missing or broken webhook handling.

Priority: P0 — Any app accepting Stripe payments without webhook verification is vulnerable to payment fraud.

Affected Stack: Next.js + Stripe, any framework with Stripe integration


BIL-02: Webhook Signature Verification

The Problem

AI code generators typically create a webhook endpoint that parses the JSON body and processes events — but skip the signature verification step. Without stripe.webhooks.constructEvent(), your endpoint accepts any POST request as a legitimate Stripe event.

// ❌ What AI tools typically generate
export async function POST(req: Request) {
  const body = await req.json();
  const event = body; // No signature verification!

  if (event.type === 'checkout.session.completed') {
    // Fulfill the order...
  }

  return new Response('OK', { status: 200 });
}

The Fix

Always verify the webhook signature using Stripe's SDK before processing any event.

// ✅ Production-safe pattern
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(req: Request) {
  const body = await req.text(); // Raw body required
  const sig = req.headers.get('stripe-signature')!;

  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(
      body,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err) {
    return new Response('Invalid signature', { status: 400 });
  }

  // Process verified event...
  return new Response('OK', { status: 200 });
}

References


BIL-03: Raw Body Preservation

The Problem

Stripe signature verification requires the raw request body — not a parsed JSON object. In Next.js App Router, calling req.json() before constructEvent() silently breaks signature verification because the body is consumed and re-serialized.

AI tools almost always generate req.json() first, making the subsequent constructEvent() call fail or — worse — appear to work in development but fail in production with different serialization.

The Fix

Use req.text() to get the raw body string, then pass it directly to constructEvent().

// ✅ Raw body preserved
const rawBody = await req.text();
const event = stripe.webhooks.constructEvent(rawBody, sig, secret);

In Next.js Pages Router, disable body parsing for the webhook route:

export const config = { api: { bodyParser: false } };

BIL-04: Idempotent Webhook Processing

The Problem

Stripe may send the same webhook event multiple times (retries, network issues). Without idempotency handling, your app may fulfill the same order twice, grant double credits, or create duplicate subscriptions.

The Fix

Store processed event IDs and check before processing. Use the Stripe event ID as the idempotency key.

// ✅ Idempotency check
const existing = await db.webhookEvents.findUnique({
  where: { stripeEventId: event.id }
});
if (existing) {
  return new Response('Already processed', { status: 200 });
}

// Process event...
await db.webhookEvents.create({
  data: { stripeEventId: event.id, type: event.type }
});

ADM-15: Admin Webhook Verification

This check covers the same vulnerability as BIL-02 but in the admin context — admin-initiated webhooks (e.g., for subscription management, user provisioning) must also verify signatures. The canonical fix is identical.


Related Checks


Is your app safe? Run Free Scan →