# Webhook signature verification is implemented on all payment webhooks

- **Pattern:** `ab-001154` (`ecommerce-payment-security.payment-integration.webhook-signature-verify`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001154
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001154)

## Why it matters

Without webhook signature verification, any HTTP client on the internet can POST a fabricated `payment_intent.succeeded` event to your webhook endpoint and trigger order fulfillment, subscription activation, or credit grants — without paying anything. CWE-345 (Insufficient Verification of Data Authenticity) and CWE-352 (CSRF) both apply: an attacker replays or forges events, and your server acts on them as if they came from Stripe. PCI-DSS 4.0 Req 6.4 requires that payment event handling be protected against unauthorized manipulation. OWASP A07 (Identification and Authentication Failures) flags accepting unverified events as a complete authentication bypass.

## Severity rationale

Critical because an unverified webhook endpoint lets any actor forge a payment confirmation event and receive goods or services without being charged.

## Remediation

Call `stripe.webhooks.constructEvent()` as the absolute first step after reading the raw body. If it throws, return 400 immediately — never fall through to business logic.

```ts
// app/api/webhooks/stripe/route.ts
export async function POST(req: Request) {
  const body = await req.text()  // raw — must not be pre-parsed
  const sig = (await headers()).get('stripe-signature')!

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

  // Business logic only reaches here after verification
  switch (event.type) { /* ... */ }
  return new Response('OK', { status: 200 })
}
```

Do not wrap `constructEvent` in a catch that swallows the error and continues — that negates the entire protection.

## Detection

- **ID:** `webhook-signature-verify`
- **Severity:** `critical`
- **What to look for:** Count every webhook endpoint file (search for route paths containing "webhook", "hook", "stripe", "paypal", "square" in API route directories). For each webhook handler, classify it as verified or unverified: check whether the request body and a signature header are passed to the provider's verification method before any business logic runs. For Stripe, look for `stripe.webhooks.constructEvent()`. For PayPal, look for `paypal.webhooks.verifyWebhookSignature()`. Check that if verification throws or returns an error, the handler returns a non-2xx response immediately without processing the payload.
- **Pass criteria:** Every payment webhook endpoint verifies the request signature using the provider's official SDK method. No more than 0 webhook handlers should lack signature verification. If verification fails or the signature header is missing, the handler returns 400 or 401 without executing any business logic. Report even on pass: "X of Y webhook endpoints have signature verification."
- **Fail criteria:** Any webhook endpoint reads and acts on the request body without first verifying the signature. This includes endpoints that verify some events but not others based on event type.
- **Do NOT pass when:** Signature verification code exists but is wrapped in a try/catch that swallows the error and continues processing — this is NOT a pass because unverified payloads still reach business logic.
- **Skip (N/A) when:** The project has no webhook integration detected — no webhook handler files found and no webhook-related dependencies or routes identified.
- **Detail on fail:** Name each unverified webhook endpoint and the missing verification call. Example: `"app/api/webhooks/stripe/route.ts processes payment.succeeded events without calling stripe.webhooks.constructEvent() — any HTTP client can spoof payment events"`
- **Cross-reference:** The API Security audit covers webhook endpoint hardening and request authentication patterns beyond payment-specific webhooks.
- **Remediation:** Implement signature verification as the very first step in every webhook handler:

  ```ts
  // app/api/webhooks/stripe/route.ts
  import Stripe from 'stripe'
  import { headers } from 'next/headers'

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

  export async function POST(req: Request) {
    const body = await req.text() // raw body — MUST NOT be pre-parsed
    const sig = 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 })
    }

    // Only now process the event
    switch (event.type) { /* ... */ }

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

  Store your webhook signing secret (`whsec_*`) in an environment variable — never hardcode it.

## External references

- cwe CWE-345
- cwe CWE-352
- owasp A07
- pci-dss Req 6.4

Taxons: cryptography-and-secrets, access-control

HTML version: https://auditbuffet.com/patterns/ab-001154
