webhook-safety-review
Skill0 starsChanged 4 months agoMITView on GitHub
Review incoming webhook handlers for the four failures that cost money — unverified signatures, missing idempotency, work done before acknowledging, and errors that make the sender retry forever. Use when adding or changing a webhook, integrating Stripe/GitHub/Slack, or after a duplicate charge or missed event.
In this repository19 other files it gives its agents.
- client-secret-sweepSkill
- codebase-onboarding-mapSkill
- commitSkill
- crawlability-auditSkill
- dependency-outage-auditSkill
- deploy-drift-checkSkill
- empty-state-auditSkill
- env-drift-checkSkill
- fake-data-guardSkill
- honest-metrics-auditSkill
- migration-safety-reviewSkill
- money-math-reviewSkill
- n-plus-one-finderSkill
- prompt-injection-reviewSkill
- pr-reviewerSkill
- release-notes-writerSkill
- retry-safety-reviewSkill
- route-auth-sweepSkill
- timezone-reviewSkill
The file
We split this skill into the rules it gives an agent
8 rules. About 0 are common, 7 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈7
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 39
A signature is verified at all: No verification means anyone who learns the URL can post a fake payment_succeeded and grant themselves …
- Distinctive: line 57
If verification is missing on a handler that grants anything, stop and report it immediately.
- Distinctive: line 80
Check the guard is on the RIGHT key.
- Distinctive: line 108
Read the catch blocks specifically.
- Distinctive: line 112
Treat the body as attacker-controlled even after signature verification, because a legitimate sender still sends fields you should not …
- Distinctive: line 149
Never trust money amounts from a payload, even a signed one.
- Distinctive: line 150
Do not report a handler safe without citing where each of the four properties is enforced.
- Distinctive: line 156
Keep this line and others can find it too.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Webhook Safety Review
Install
Save this file as ~/.claude/skills/webhook-safety-review/SKILL.md, or
.claude/skills/webhook-safety-review/SKILL.md to scope it to one repo. Claude
Code auto-discovers it. Invoke with /webhook-safety-review or by asking "is my
Stripe webhook safe?".
Why this exists
A webhook handler is an unauthenticated public endpoint that a stranger can call, which reacts to it by granting entitlements or moving money. Almost every serious bug in one falls into four buckets, and all four are invisible in normal testing because your own test events are well-behaved.
Rank findings by money and access, not by tidiness.
Step 1 — Find every handler
rg --files -g '**/webhook*' -g '**/webhooks/**' -g '**/hooks/**'
rg -ln "stripe|github|slack|shopify|twilio|resend|clerk|paddle" -g '**/route.ts' -g '**/*.py' -g '**/*.go'For each, note the sender and what it causes: creating an order, granting a licence, sending mail, updating a subscription. That consequence sets the severity of everything below.
Step 2 — Signature verification, before anything else
The single highest-severity check. Confirm, with file and line:
- A signature is verified at all. No verification means anyone who learns
the URL can post a fake
payment_succeededand grant themselves goods. - It runs BEFORE the body is parsed or acted on. Verifying after you have already created a record is decoration.
- It uses the RAW body. Frameworks that auto-parse JSON break signatures silently, because re-serialising changes bytes. Look for the explicit raw-body escape hatch, and be suspicious if you cannot find one.
- It is constant-time.
===on a signature is a timing oracle; expecttimingSafeEqualor the SDK's own verifier. - The secret comes from the environment, is not a fallback default, and is not the publishable/public key by mistake.
- A timestamp is checked where the sender provides one, so a captured request cannot be replayed later.
rg -n "constructEvent|verifySignature|timingSafeEqual|createHmac|x-hub-signature|stripe-signature" -g '**/route.ts'If verification is missing on a handler that grants anything, stop and report it immediately. Nothing else in this review matters as much.
Step 3 — Idempotency
Senders retry. Assume every event arrives more than once. Stripe retries for days; a network blip mid-response guarantees a duplicate.
For each handler, find what makes reprocessing safe:
- A unique constraint on the event id, or on a natural key like the order id.
- An upsert rather than a create.
- A processed-events table checked before doing the work.
rg -n "@@unique|@unique|ON CONFLICT|upsert|findUnique.*event" The failure is not theoretical: without this, a retried checkout.completed
creates a second order, a second licence, or a second payout. If you have ever
seen a duplicate charge or a double-granted entitlement, this is where it came
from.
Check the guard is on the RIGHT key. Deduplicating on your own generated id while the sender retries with the same event id protects nothing.
Step 4 — Acknowledge fast, work after
Senders time out, and a timeout is treated as failure, so slow work causes duplicate deliveries and eventually disables the endpoint.
Flag any handler doing heavy work inline before responding: sending email, generating files, calling other APIs, long transactions. The shape you want is verify, record, return 200, then process out of band (a queue, or a platform-native after-response hook).
Step 5 — Return the right status
This is where handlers quietly become incident generators:
- 200 — received and either handled or safely ignored. Return 200 for event types you do not care about, otherwise the sender retries them forever.
- 400 — signature invalid or body malformed. Permanent failure; do not invite a retry.
- 5xx — YOUR transient failure, where you genuinely want a retry.
The classic bug is a handler that throws on an unrecognised event type, returns 500, and gets retried forever until the provider disables the endpoint. The second classic is catching everything and returning 200, so real failures are silently dropped and the sender never retries something you actually needed.
Read the catch blocks specifically. Both mistakes live there.
Step 6 — Check what the handler trusts from the payload
Treat the body as attacker-controlled even after signature verification, because a legitimate sender still sends fields you should not blindly trust.
- Never trust an amount, price, or currency from the payload. Re-read it from your own record or from the provider's API using the id.
- Resolve the user from your stored mapping, not from an email in the body.
- Validate the shape with a schema; do not index into nested fields hopefully.
Step 7 — Report
Handlers reviewed: <n>
CRITICAL
<file:line> <endpoint> — <no signature check | verified after acting | trusts payload amount>
attack: <what a stranger posts, and what they get>
HIGH
<file:line> — not idempotent on <key> -> retry causes <duplicate order/licence/charge>
MEDIUM
<file:line> — returns <status> for <case> -> <infinite retry | silent drop>
<file:line> — <slow work> before responding
VERIFIED SAFE
<file:line> — signature verified pre-parse, idempotent on <key>, correct statusesFor every CRITICAL, write the concrete attack in one sentence. "Anyone who learns this URL can post a fake payment event and receive a paid product for free" gets fixed today.
Rules
- Signature verification before parsing. Everything else is secondary.
- Assume duplicate delivery, always. Idempotency is not optional.
- 200 means "received", not "succeeded". Unknown event types get 200.
- Never trust money amounts from a payload, even a signed one.
- Do not report a handler safe without citing where each of the four properties is enforced.
From Toolbay. Free to use, modify, and share. Keep this line and others can find it too.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

