Stripe Webhook Replay Attacks and Signature Pitfalls
Stripe signs webhook payloads with a secret and a timestamp. Apps that skip verification—or verify incorrectly—will believe anything that looks like JSON from the public internet. Replay is the boring cousin of forgery: a valid past event, sent again, triggers a second side effect.
Find the listener
Hunt paths like /webhooks/stripe, /api/stripe/webhook, /billing/hooks. Staging clones often reuse production secrets or disable signature checks "temporarily." If the program includes staging, start there. DNS history and JS bundles sometimes reveal an old worker hostname still live.
Capture a legitimate event from your own Stripe test mode: checkout.session.completed, invoice.paid, customer.subscription.updated. Save raw body bytes. Signature schemes that hash a parsed-and-reserialized body will drift; Stripe's docs insist on the exact raw payload. Middleware that parses JSON before verification is a classic self-own—mention it if you can show verification fails on the true body but a wrong path still credits the user.
Attack checklist
- No signature header required — drop
Stripe-Signatureentirely and POST a craftedevent.idthat grants premium. - Secret confusion — endpoint accepts test-mode signatures on live products, or verifies against a hardcoded example secret from a tutorial.
- Timestamp ignored — replay an old valid capture hours later; if
t=is not checked, replay works forever within log retention. - Idempotency missing — signature verifies, but processing
evt_123twice credits the wallet twice. - Event type confusion — handler switches on a client-influenced field instead of
type, or trustsdata.object.metadatablindly for account ids.
For idempotency failures, the clean proof is two identical POSTs with the same event id and a canary customer you own. Show balance or entitlement went 0→1→2. Stop there. Do not spray random customer ids.
Cross-account angle: some apps map client_reference_id or metadata from the event into account upgrades without checking that the Stripe customer belongs to the session user. Pair a webhook you trigger in test mode with metadata pointing at another test user. Connect platforms add complexity—account webhooks versus application webhooks—confirm which secret the endpoint uses before claiming a forge.
Operational pitfalls hunters hit
Stripe CLI forwarding to localhost is fine for learning; for bounty, hit the deployed URL the program authorized. Rate limits and automatic retries from Stripe can duplicate your manual replay—tag canaries clearly so you do not mis-count side effects. Logging endpoints that echo the raw event into a ticket system may create secondary PII issues; keep screenshots redacted.
Reporting without becoming a payments headache
Use Stripe test mode exclusively unless the program provides a live sandbox and explicit permission. Never replay against events that touch real money movement you do not own. If you stumble on a live secret in a public repo, report exposure first and pause active exploitation.
Triage likes: raw request, response codes, the duplicate side effect, and whether Stripe-Signature was validated with t tolerance. Recommend official verification libraries, reject outside tolerance, store processed event.id values, rotate secrets after leaks, and keep webhook secrets out of mobile apps and frontend repos. Signature verification without idempotency is half a fix—say that plainly.