Fairy
Resources

How to Avoid: Stripe Webhook Missing Signature Verification

August 8, 2026 · 6-minute read · Fairy

The short answer

To prevent Stripe webhook missing signature verification in AI-generated code, always call stripe.webhooks.constructEvent(rawBody, signature, webhookSecret) before processing any event. Use the raw request body (not parsed JSON), wrap in try/catch, and reject requests that fail verification. This stops attackers from forging webhook events to grant fake subscriptions or trigger unauthorized refunds.

The Direct Answer: Always Verify Before Processing

Stripe webhook missing signature verification is a critical security vulnerability where your webhook endpoint processes events without confirming they actually came from Stripe. The fix is straightforward: call stripe.webhooks.constructEvent(rawBody, signature, webhookSecret) as the first step in your handler, before any business logic executes.

This single check prevents attackers from forging webhook events that could grant unauthorized subscriptions, trigger fake refunds, or initiate order fulfillment for payments that never happened.

Why AI Generates This Vulnerability

AI code assistants consistently produce Stripe webhook handlers that skip signature verification. This happens for predictable reasons rooted in how these models learn.

Training Data Prioritizes Functionality Over Security

Most Stripe webhook tutorials focus on getting the integration working. The signature verification step is often mentioned as a "production consideration" or appears in a separate security section that AI models don't consistently associate with the core webhook pattern.

When you ask an AI to "create a Stripe webhook handler," it optimizes for the happy path: receive event, check event type, execute business logic. The code works perfectly in development—and is completely vulnerable in production.

The Functional Pattern Looks Complete

Here's what AI typically generates:

// VULNERABLE: AI-generated webhook without verification
app.post('/webhook', express.json(), async (req, res) => {
  const event = req.body;
  
  switch (event.type) {
    case 'checkout.session.completed':
      await fulfillOrder(event.data.object);
      break;
    case 'customer.subscription.created':
      await grantAccess(event.data.object);
      break;
  }
  
  res.json({ received: true });
});

This code handles events correctly. It parses the webhook payload, routes based on event type, and executes the appropriate business logic. Nothing about it signals "incomplete" to an AI evaluating its own output.

But anyone can POST to this endpoint. An attacker could send:

{
  "type": "checkout.session.completed",
  "data": {
    "object": {
      "customer": "cus_attacker",
      "subscription": "sub_premium_lifetime"
    }
  }
}

Your server would grant premium access without any payment ever occurring.

The Correct Pattern: Verification First

Proper webhook handling requires cryptographic verification before any event processing:

// SECURE: Webhook with signature verification
app.post('/webhook', 
  express.raw({ type: 'application/json' }), // Raw body required
  async (req, res) => {
    const signature = req.headers['stripe-signature'];
    
    let event;
    try {
      event = stripe.webhooks.constructEvent(
        req.body,           // Must be raw body, not parsed JSON
        signature,
        process.env.STRIPE_WEBHOOK_SECRET
      );
    } catch (err) {
      console.error('Webhook signature verification failed:', err.message);
      return res.status(400).send(`Webhook Error: ${err.message}`);
    }
    
    // Now safe to process - event is verified
    switch (event.type) {
      case 'checkout.session.completed':
        await fulfillOrder(event.data.object);
        break;
      case 'customer.subscription.created':
        await grantAccess(event.data.object);
        break;
    }
    
    res.json({ received: true });
  }
);

Critical Detail: Raw Body Requirement

Notice express.raw({ type: 'application/json' }) instead of express.json(). This is where many AI-assisted fixes still fail.

Stripe's signature is computed over the exact bytes sent in the request. If you parse the JSON first with express.json(), then try to verify, the signature check will fail even for legitimate Stripe requests. JSON serialization doesn't preserve exact byte order, whitespace, or key ordering.

In frameworks other than Express, ensure you're accessing the unparsed request body:

# Python/Flask - use request.data, not request.json
@app.route('/webhook', methods=['POST'])
def webhook():
    payload = request.data  # Raw bytes
    sig = request.headers.get('Stripe-Signature')
    
    try:
        event = stripe.Webhook.construct_event(
            payload, sig, webhook_secret
        )
    except stripe.error.SignatureVerificationError:
        return 'Invalid signature', 400
// Next.js API route - disable body parsing
export const config = {
  api: { bodyParser: false }
};

export default async function handler(req, res) {
  const buf = await buffer(req);
  const sig = req.headers['stripe-signature'];
  
  let event;
  try {
    event = stripe.webhooks.constructEvent(buf, sig, webhookSecret);
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }
  // Process verified event...
}

Detection: Finding This Vulnerability in Your Codebase

Manual Code Review Checklist

Search your codebase for these signals:

  1. Webhook route handlers - Look for routes matching /webhook, /stripe-webhook, /api/webhooks/stripe
  2. Missing constructEvent - If the handler processes req.body or event.data.object without calling stripe.webhooks.constructEvent(), it's vulnerable
  3. Parsed body middleware - If express.json() runs before the webhook route, verification may silently fail
  4. Missing webhook secret - Check for STRIPE_WEBHOOK_SECRET in environment variables

Automated Detection with AI Code Review

Tools like Fairy Scout specifically flag this pattern. The detection looks for webhook handlers that process Stripe events without the constructEvent call, catching both the missing verification and the raw body requirement.

When reviewing AI-generated payment code, signature verification should be your first checkpoint. It's a binary check: either constructEvent runs before processing, or the endpoint is vulnerable.

The Related Vulnerability: Missing Idempotency

Signature verification prevents forged events. But even verified events can cause problems if processed multiple times.

Stripe retries webhooks when your endpoint returns a timeout or 5xx error. Without idempotency handling, a network hiccup during response could result in:

The fix is to track processed event IDs:

app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  const sig = req.headers['stripe-signature'];
  
  let event;
  try {
    event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }
  
  // Idempotency check - skip if already processed
  if (await isEventProcessed(event.id)) {
    return res.json({ received: true, duplicate: true });
  }
  
  // Process the event
  switch (event.type) {
    case 'checkout.session.completed':
      await fulfillOrder(event.data.object);
      break;
  }
  
  // Mark as processed after successful handling
  await markEventProcessed(event.id);
  
  res.json({ received: true });
});

AI-generated code almost never includes idempotency handling. It's invisible in happy-path testing and only manifests during production incidents.

Another Trap: Premature Fulfillment

A related pattern AI produces incorrectly: triggering fulfillment on PaymentIntent.create instead of payment_intent.succeeded or checkout.session.completed.

// WRONG: Fulfilling before payment confirms
case 'payment_intent.created':
  await grantPremiumAccess(event.data.object);
  break;

// CORRECT: Fulfilling after payment succeeds  
case 'payment_intent.succeeded':
  await grantPremiumAccess(event.data.object);
  break;

The created event fires when a payment intent is initialized—before the customer enters card details, before 3D Secure, before the bank approves the charge. Granting access here means users get your product without paying.

Implementation Checklist

Before deploying any Stripe webhook handler to production:

Testing Your Webhook Security

Verify Legitimate Events Work

Use the Stripe CLI to send test webhooks:

stripe listen --forward-to localhost:3000/webhook
stripe trigger checkout.session.completed

Your endpoint should process these successfully.

Verify Forged Events Are Rejected

Send a request without a valid signature:

curl -X POST http://localhost:3000/webhook \
  -H "Content-Type: application/json" \
  -d '{"type":"checkout.session.completed","data":{"object":{}}}'

Your endpoint should return 400, not process the event.

Why This Matters for AI-Generated Code

Payment handling is exactly the kind of code AI generates confidently but incompletely. The functional flow—receive event, route by type, execute business logic—is well-represented in training data. The security infrastructure that makes it production-safe is not.

Every Stripe webhook handler generated by AI should be treated as suspicious until signature verification is confirmed. This isn't about AI being bad at code; it's about AI optimizing for the observable behavior in its training data, which rarely includes attack scenarios.

For teams shipping AI-generated payment integrations, systematic review of webhook handlers is essential. Fairy's code verification specifically targets these payment patterns, catching missing signature verification before vulnerable code reaches production.

The cost of this vulnerability is direct financial loss. Unlike performance bugs or UI issues, a forged webhook can grant unauthorized access or trigger fulfillment for payments that never occurred. Verification takes three lines of code. Skipping it exposes your entire payment flow.

Frequently asked questions

Why does AI often generate Stripe webhooks without signature verification?

AI models learn from tutorials and examples that often omit security steps for brevity. The basic webhook pattern works functionally without verification, so AI produces code that handles events correctly but lacks the cryptographic check that proves requests came from Stripe.

What can attackers do if my Stripe webhook lacks signature verification?

Attackers can POST fake webhook events to your endpoint, triggering unauthorized actions like granting premium subscriptions, processing fake refunds, or initiating order fulfillment. Your server cannot distinguish forged events from legitimate Stripe requests.

Why must I use the raw body instead of parsed JSON for Stripe verification?

Stripe's signature is computed over the exact bytes sent in the request. If you parse the JSON first, serializing it back may change whitespace or key ordering, causing verification to fail even for legitimate requests.

Do I need idempotency handling in addition to signature verification?

Yes. Stripe retries webhooks on timeout or 5xx responses. Without checking event.id for duplicates, you may process the same event multiple times, causing double fulfillment or duplicate charges.

How do I test that my webhook signature verification works correctly?

Use the Stripe CLI to send test webhooks locally with valid signatures. Also test with a forged POST request (no signature or wrong signature) to confirm your endpoint rejects it with a 400 or 401 status.


Have AI-generated work you’d want verified? Connect with a Fairy → or run a free check with Scout.

More resources