stripe-webhook-signature-verification — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited stripe-webhook-signature-verification (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
What it is: The security check that proves an incoming Stripe webhook was signed by Stripe before any payment logic runs. Mental model: The raw request body, signature header, and webhook secret form one verification tuple; change any part and the event is untrusted. Why it exists: Webhook routes are public endpoints that can trigger billing and fulfillment, so authenticity has to be established before routing. What it is NOT: It is not payment-event routing, general HTTP signature validation, or Stripe API usage outside webhook delivery. Adjacent concepts: HMAC verification, raw request bodies, replay tolerance, idempotency keys. One-line analogy: It is the seal check before opening the payment envelope. Common misconception: Parsing JSON first is harmless; transforming the raw bytes invalidates the signature comparison.
stripe.webhooks.constructEvent() requires the unparsed Buffer from the request body, and how Next.js App Router routes expose it via request.arrayBuffer()constructEvent(rawBody, signature, secret) reconstructs and compares the Stripe signature internallyt= timestamp embedded in the stripe-signature header; when to tighten itSTRIPE_WEBHOOK_SECRET for production vs whsec_... from the Stripe CLI --forward-to session in development; why they must never be swappedevent.id in Postgres before processing so a retried delivery does not double-charge or double-fulfillA webhook that skips signature verification is an unauthenticated public endpoint that can trigger payment processing. The verification step is load-bearing security, not a convenience check. Stripe's SDK makes verification a single call, but two failure modes are common in practice: the request body gets parsed (by a body-parser middleware) before the raw bytes reach the verification call, which silently corrupts the HMAC comparison; and the wrong webhook secret is loaded from environment variables, producing a 400 that is hard to distinguish from a replay rejection. Both failures look the same to the caller — a rejected webhook — and both are invisible until a real event is dropped.
const rawBody = Buffer.from(await request.arrayBuffer()). Do NOT pass await request.json() or await request.text() — both transform the bytes. import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const sig = request.headers.get("stripe-signature") ?? "";
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
rawBody,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
return Response.json({ error: "Signature verification failed" }, { status: 400 });
} INSERT INTO webhook_events (event_id, processed_at)
VALUES ($1, now())
ON CONFLICT (event_id) DO NOTHING
RETURNING event_id;If the RETURNING clause returns no rows, the event was already processed — return 200 immediately without re-running side effects.
payment-provider-router.| Failure | Symptom | Fix |
|---|---|---|
| Body parsed before verification | 400 on every real Stripe event | Use arrayBuffer(), not json() or text() |
| Wrong webhook secret | 400 with "No signatures found matching the expected signature" | Verify STRIPE_WEBHOOK_SECRET matches the endpoint in the Stripe dashboard |
| Replay attack | 400 with "Timestamp too old" | Legitimate if tolerance is tight; check t= value in the stripe-signature header |
| Secret from wrong environment | Events verify in dev but fail in production | Use per-environment secrets; never share between environments |
| Use instead | When |
|---|---|
payment-provider-router | You have a verified event and need to decide which handler processes it |
nextjs-server-action-validation | You are validating user-submitted form data, not a Stripe webhook |
| (a generic HTTP signature skill) | You are verifying webhooks from a non-Stripe provider with a different signing scheme |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.