Webhook signatures verified
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.
// 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:
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 — Insufficient Verification of Data Authenticity
- cwe · CWE-347 — Improper Verification of Cryptographic Signature
- owasp:2021 · A02 — Cryptographic Failures
- owasp:2023 · API8 — Security Misconfiguration
Taxons
History
- 2026-04-18·v1.0.0·Initial import from api-security·automated