# Webhook signatures verified

- **Pattern:** `ab-000396` (`api-security.design-security.webhook-signatures`)
- **Severity:** info
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000396
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000396)

## Why it matters

Unsigned webhooks let any attacker impersonate your event source and inject arbitrary payloads. CWE-345 (Insufficient Verification of Data Authenticity) and CWE-347 (Improper Verification of Cryptographic Signature) describe the failure. OWASP API Security Top 10 2023 API8 (Security Misconfiguration) covers unsigned webhook receipt as a trust boundary violation. Platforms like Stripe, GitHub, and Twilio all sign their webhooks with HMAC-SHA256 for this reason. Without signature verification, attackers who discover your webhook endpoint can send fake events — triggering account upgrades, order fulfillments, or permission changes — by simply POSTing correctly shaped JSON. Timing-safe comparison is required; string equality is vulnerable to timing attacks.

## Severity rationale

Info severity because webhook forgery requires knowing your endpoint URL, but the impact — injecting arbitrary business events — can be critical to business logic.

## Remediation

Compute and compare the HMAC-SHA256 signature using `crypto.timingSafeEqual` — regular string comparison leaks timing information that can be used to forge valid signatures bit by bit.

```ts
// src/app/api/webhooks/route.ts
import crypto from 'crypto'

export async function POST(req: Request) {
  const rawBody = await req.text()  // read as text before parsing
  const sig = req.headers.get('x-webhook-signature') ?? ''

  const expected = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET!)
    .update(rawBody)
    .digest('hex')

  const sigBuffer = Buffer.from(sig, 'hex')
  const expectedBuffer = Buffer.from(expected, 'hex')

  // timingSafeEqual requires same-length buffers
  if (sigBuffer.length !== expectedBuffer.length ||
      !crypto.timingSafeEqual(sigBuffer, expectedBuffer)) {
    return new Response('Unauthorized', { status: 401 })
  }

  const payload = JSON.parse(rawBody)
  // ... process verified payload
}
```

## Detection

- **ID:** `webhook-signatures`
- **Severity:** `info`
- **What to look for:** Enumerate every relevant item. If the API sends webhooks or receives webhooks from external services, check whether webhook payloads are signed and verified. Look for HMAC-SHA256 signature generation and verification logic.
- **Pass criteria:** At least 1 of the following conditions is met. All webhooks are signed using HMAC-SHA256 or equivalent. The receiving endpoint verifies the signature before processing the payload.
- **Fail criteria:** Webhooks are sent or received without signature verification.
- **Skip (N/A) when:** The API does not send or receive webhooks.
- **Detail on fail:** `"Webhooks sent without signature — recipients cannot verify payload authenticity"` or `"Webhook endpoint does not verify HMAC signature before processing payload"`
- **Remediation:** Implement webhook signing and verification:

  ```ts
  import crypto from 'crypto'

  // Sending webhooks
  const sendWebhook = async (payload, webhookUrl, secret) => {
    const signature = crypto
      .createHmac('sha256', secret)
      .update(JSON.stringify(payload))
      .digest('hex')

    await fetch(webhookUrl, {
      method: 'POST',
      headers: {
        'X-Signature': signature,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(payload)
    })
  }

  // Receiving webhooks
  app.post('/api/webhooks/external', (req, res) => {
    const signature = req.headers['x-signature']
    const computed = crypto
      .createHmac('sha256', process.env.WEBHOOK_SECRET)
      .update(JSON.stringify(req.body))
      .digest('hex')

    if (signature !== computed) {
      return res.status(401).json({ error: 'Invalid signature' })
    }

    // Process webhook
    res.json({ success: true })
  })
  ```

## External references

- cwe CWE-345
- cwe CWE-347
- owasp A02
- owasp API8

Taxons: cryptography-and-secrets, access-control

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