Skip to content
SlashClone
Made by@shipcheck

Stripe webhook hardening

@shipcheck/stripe-webhook-hardening

Find your Stripe webhook handlers and harden them: verify the Stripe-Signature header, use the raw body, keep the signing secret in env, process each event once, reject bad signatures with 400, and fulfill orders from the webhook rather than the success page. Reads your code and proposes diffs; never calls Stripe.

SkillDevOpsv1.0.0 (latest)Tested
Activity
New: be the first to try it

Real output from this kit

How this works

  1. It is a skill: written instructions Claude reads when your request matches what the skill is for. You do not call it by name; just ask for the task.
  2. While it works, it reads and changes files in the places it lists, nowhere else.
  3. It runs these commands on your computer: node scripts/webhook-scan.mjs.
  4. Nothing runs until you choose to use it, and you can remove it any time.

SKILL.md

Stripe webhook hardening

A Stripe webhook is a public URL that grants paid access. If it skips the signature check, anyone can post a fake "payment succeeded" event. If it is not idempotent, a Stripe retry gives double credits. This skill finds the handlers, checks them, and proposes small, specific diffs.

When to use

  • After adding Stripe Checkout, Payment Links, subscriptions, or Connect.
  • "Is my webhook secure?", "signature verification failed", "users got charged once but credited twice".
  • Before turning on live mode.

Steps

  1. Scan.

    node scripts/webhook-scan.mjs . --json out/webhook-scan.json --md out/webhook-scan.md

    If it finds 0 handlers, search the code for stripe-signature, constructEvent, and checkout.session.completed, and ask the user which route their Stripe dashboard points to.

  2. Read every handler file in full. The scan is a pointer, not the verdict. Confirm each finding against the code and check what the scan cannot:

    • Fulfillment source: is access granted on the success page (reading session_id from the URL) instead of in the webhook? The success page is user-controlled. It may show "thanks", but only the webhook grants access.
    • Trusting metadata blindly: amounts and prices should come from the Stripe event or a Stripe API lookup, never from client-sent values.
    • Slow work before responding: long jobs (emails, AI calls) should be queued so the handler returns 2xx within a few seconds, or Stripe retries.
    • Event coverage: subscriptions need customer.subscription.updated and .deleted, and invoice.payment_failed, or cancelled users keep access.
  3. Report using the format below. High first.
  4. Propose fixes as diffs, one finding at a time, and apply each only after the user says yes. Use the patterns below for their framework.
  5. If idempotency needs a table, write a new migration (for example create table processed_events (id text primary key, created_at timestamptz default now())) and show it. The user applies it.
  6. Test plan. Tell the user to run stripe listen --forward-to localhost:3000/api/webhooks/stripe and stripe trigger checkout.session.completed themselves, then resend the same event from the dashboard to confirm it is processed once.

Fix patterns

Next.js App Router:

export async function POST(req: Request) {
  const body = await req.text(); // raw body, never req.json()
  const sig = req.headers.get("stripe-signature") ?? "";
  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(body, sig, webhookSecret); // webhookSecret from STRIPE_WEBHOOK_SECRET
  } catch {
    return new Response("Invalid signature", { status: 400 });
  }
  // insert event.id into processed_events; on conflict return 200 and stop
}

Express: register express.raw({ type: "application/json" }) on the webhook route, before app.use(express.json()). Edge or Supabase Edge Functions: await stripe.webhooks.constructEventAsync(body, sig, secret, undefined, Stripe.createSubtleCryptoProvider()).

Output format

# Stripe webhooks: <project>
Handlers: 1 | High 2 | Medium 1 | Low 0

## app/api/webhooks/stripe/route.ts
- HIGH signature: no constructEvent. Anyone can fake a paid checkout. Fix: diff 1.
- HIGH raw-body: uses req.json(). Fix: diff 1.
- MEDIUM idempotency: retries double the credits. Fix: diff 2 plus migration.
- Fulfillment: success page also grants access (app/success/page.tsx:14). Fix: diff 3.

Guardrails

  • Never call the Stripe API, create webhook endpoints, or roll secrets. Tell the user where to do it in the dashboard.
  • Never print or paste a signing secret or API key. If one is hardcoded, report the file and line only, and say it must be rolled.
  • Never "fix" a failing signature by removing verification or by switching to test mode.
  • Use test mode for every test you suggest. Live mode is the user's call.

README.md

Stripe webhook hardening

Finds your Stripe webhook handlers and checks the things that let people fake payments or get credited twice. Then it proposes small diffs for your framework, one at a time.

For: anyone who added Stripe Checkout or subscriptions to an app, especially with a coding agent's help.

Example

Input: a Next.js route that does const event = await req.json() and marks the order paid.

Output (out/webhook-scan.md):

Handlers found: 1 | High: 2 | Medium: 1 | Low: 0

## app/api/webhooks/stripe/route.ts
| Check       | Result          | Note                                        |
| signature   | FAIL (high)     | Anyone who finds this URL can post a fake   |
|             |                 | checkout.session.completed.                 |
| raw-body    | FAIL (high)     | Reads the body as parsed JSON.              |
| idempotency | FAIL (medium)   | Retries can double up orders or credits.    |

Then Claude reads the handler in full, checks things a scan cannot (like access granted on the success page), and shows a diff for each fix. Try it on the bundled example: node scripts/webhook-scan.mjs examples/project.

What it checks

Signature verification, raw body, signing secret in env, idempotency on event.id, 400 on bad signatures, event type filtering, edge runtime support, and fulfillment from the webhook rather than the success page. Works best with Node (Next.js, Express, Supabase Edge Functions); Python, Ruby, Go and PHP handlers are detected with lighter checks.

What it can access

  • Reads your project's code. Writes the report to out/.
  • Edits handler files in app/, src/, pages/, server/, api/, or supabase/functions/, and adds a migration, only after you say yes to each diff.
  • No network. It never calls Stripe and needs no keys.

Setup

None. Needs Node 22 or later. The Stripe CLI is handy for testing your fixes but not required.

Files

  • README.md
  • SKILL.md
  • examples/project/app/api/webhooks/stripe/route.ts
  • examples/project/server/stripe-webhook.js
  • kit.json
  • scripts/webhook-scan.mjs

Did it work?

0worked for them0did not work

Sign in to say whether it worked for you.

Nobody has said yet. Tried it? Tell the next person.