From a79b10a8dd2ac74b7d3bab8f70382268e16a7993 Mon Sep 17 00:00:00 2001 From: Katniss Date: Fri, 5 Jun 2026 22:27:30 -0700 Subject: [PATCH] fix(stripe): enforce 5-minute timestamp tolerance to prevent webhook replay attacks Stripe's own SDK and documentation require implementations to reject webhook events whose timestamp (t=) is more than 5 minutes from the current wall clock, preventing replay attacks where an attacker captures a valid (payload, signature) pair and re-submits it later. The previous verifyStripeSignature() verified the HMAC correctly but never compared the timestamp against Date.now(), so a captured webhook could be replayed indefinitely and re-trigger business logic (refunds, orders marked paid, etc.). Fix: add a 300-second (5 min) tolerance check, matching the default in Stripe's official stripe-node SDK, before computing the HMAC. Also validate that t= is a finite number to guard against NaN-based bypasses. Ref: https://docs.stripe.com/webhooks/signatures#replay-prevention --- packages/payments/stripe/src/index.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/payments/stripe/src/index.ts b/packages/payments/stripe/src/index.ts index acb0bc6b..446959a2 100644 --- a/packages/payments/stripe/src/index.ts +++ b/packages/payments/stripe/src/index.ts @@ -184,6 +184,21 @@ function verifyStripeSignature(rawBody: string, header: string, secret: string): throw new Error('Stripe-Signature missing t or v1'); } + // Reject replayed webhooks per Stripe's guidance: + // https://docs.stripe.com/webhooks/signatures#replay-prevention + const TOLERANCE_SECONDS = 300; // 5 minutes — matches Stripe's own SDK default + const ts = Number(timestamp); + if (!Number.isFinite(ts)) { + throw new Error('Stripe-Signature t is not numeric'); + } + const skew = Math.abs(Math.floor(Date.now() / 1000) - ts); + if (skew > TOLERANCE_SECONDS) { + throw new Error( + `Stripe webhook timestamp ${ts} is outside the ${TOLERANCE_SECONDS}s tolerance (skew: ${skew}s). ` + + 'Reject to prevent replay attacks.', + ); + } + const expected = createHmac('sha256', secret) .update(`${timestamp}.${rawBody}`) .digest('hex');