An unverified webhook endpoint is an open forged-event injection point: anyone who discovers the URL can POST a payment_intent.succeeded event with a fabricated payment amount and trigger fulfillment, subscription upgrades, or user privilege escalation without paying a cent. CWE-345 (Insufficient Verification of Data Authenticity) is the direct mapping. OWASP A07:2021 (Identification and Authentication Failures) applies: the server cannot distinguish a real Stripe event from a crafted one without signature verification. This is not a theoretical risk — Stripe explicitly documents the attack in their webhook security guide and provides constructEvent as the countermeasure.
High because a forged webhook event can trigger financial side effects — fulfillment, subscription activation, refunds — with no authentication required beyond knowing the endpoint URL.
Always verify the signature using the provider's SDK before reading the event body. For Stripe, use stripe.webhooks.constructEvent with the raw body string — not req.json(), which reparsing breaks the HMAC.
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 for HMAC
const sig = req.headers.get('stripe-signature')
if (!sig) return Response.json({ error: 'Missing signature' }, { status: 400 })
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!)
} catch {
return Response.json({ error: 'Invalid signature' }, { status: 400 })
}
// Safe to process — event is authentic
}
For Clerk or other providers using Svix, replace the Stripe call with new Webhook(secret).verify(body, headers) — the pattern is identical.
ID: ai-slop-cost-bombs.cache-idempotency.webhook-signature-verified
Severity: high
What to look for: Walk all webhook handler files. Count all webhook handlers and verify each handler calls a signature verification function before processing the body: stripe.webhooks.constructEvent(, svix.Webhook(...).verify(, crypto.createHmac(...).digest() compared to a header signature, clerk.webhooks.verify(. Count handlers that process events without verification.
Pass criteria: 100% of webhook handlers verify the signature before processing. Report: "X webhook handlers inspected, Y verify signatures, 0 unverified."
Fail criteria: At least 1 webhook handler reads the body and processes events without checking the signature header.
Skip (N/A) when: Project has 0 webhook routes detected.
Cross-reference: For broader webhook security, the API Security audit (api-security) covers signature verification patterns.
Detail on fail: "1 unverified webhook: app/api/webhooks/stripe/route.ts processes events without calling stripe.webhooks.constructEvent() — anyone who can POST to the URL can fake events"
Remediation: An unverified webhook endpoint is an open mailbox — anyone can forge events. Always verify the signature:
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
export async function POST(req: Request) {
const body = await req.text()
const sig = req.headers.get('stripe-signature')!
let event
try {
event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!)
} catch (err) {
return Response.json({ error: 'Invalid signature' }, { status: 400 })
}
// ... safe to process event
}