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
- Stripe Secret Key Exposure — BIL-01
- Server-Initiated Checkout — BIL-14
- Stripe Price ID Tampering — BIL-15
- Never Fulfill on success_url — BIL-16
Is your app safe? Run Free Scan →