# Stripe webhook replay tolerance is left at the default or set to a bounded value (600 seconds or less)

- **Pattern:** `ab-002649` (`stripe-billing-integrity.webhook-authenticity.stripe-webhook-tolerance-not-disabled`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-10-05
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002649
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002649)

## Why it matters

Stripe signs `timestamp.payload` and the SDK rejects events whose timestamp is outside a tolerance window, 300 seconds by default, so a captured request cannot be replayed hours later. The tolerance is the fourth argument to `constructEvent(payload, header, secret, tolerance)`, and the failure is overriding it to make a local test or a clock-skew error go away: `0`, a negative number, `Infinity`, or a huge literal. Passing `0` is the trap that matters in practice because several SDKs treat a falsy tolerance as "do not check the timestamp", and an assistant that was asked to "fix the timestamp outside tolerance error" will often disable the check rather than fix the server clock. With replay protection off, any signed event an attacker observes (a leaked log line, a proxy capture, a staging copy of production traffic) can be re-sent to re-run fulfillment or entitlement logic. This is CWE-294 (Authentication Bypass by Capture-replay) and CWE-345, and it is independent of whether the signature itself is verified: the signature stays valid forever once the window is disabled. Stripe documents the default tolerance and recommends against disabling timestamp checks.

## Severity rationale

High because a disabled or enormous timestamp tolerance lets a captured, validly signed event be replayed indefinitely, so one intercepted `invoice.paid` or `checkout.session.completed` can be re-submitted later to re-trigger fulfillment, but it requires the attacker to have captured a real signed payload first.

## Remediation

Remove the tolerance argument so the SDK default (300 seconds) applies, or pass an explicit value of 600 seconds or less. If you hit "Timestamp outside the tolerance zone" errors, fix the server clock (NTP) or make sure you are verifying the unmodified raw body; do not widen the window.

```ts
// default tolerance (300s) - preferred
event = stripe.webhooks.constructEvent(body, sig, secret)
// or an explicit bounded value
event = stripe.webhooks.constructEvent(body, sig, secret, 300)
```

## Detection

**The detection method for this pattern is part of AuditBuffet Pro.**

The full method (what to look for, pass and fail conditions, and how results get reported) ships inside the [Stripe Webhooks & Billing Integrity audit](https://auditbuffet.com/audits/stripe-billing-integrity). The pattern definition, severity rationale, and remediation remain openly licensed under CC-BY-4.0.

## External references

- cwe CWE-345
- cwe CWE-294
- owasp A08
- external stripe-webhooks-replay-attacks — https://docs.stripe.com/webhooks

Taxons: cryptography-and-secrets, abuse-resistance

HTML version: https://auditbuffet.com/patterns/ab-002649
