From 392350972aa5ff1a014691fac782e5a80fb64743 Mon Sep 17 00:00:00 2001 From: aiirvizionz Date: Tue, 28 Jul 2026 22:26:57 -0600 Subject: [PATCH] Guard Stripe webhook tolerance config --- packages/payments/stripe/src/index.test.ts | 16 ++++++++++++++++ packages/payments/stripe/src/index.ts | 12 ++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/payments/stripe/src/index.test.ts b/packages/payments/stripe/src/index.test.ts index 44569921..2893d514 100644 --- a/packages/payments/stripe/src/index.test.ts +++ b/packages/payments/stripe/src/index.test.ts @@ -137,6 +137,22 @@ describe('payment-stripe', () => { { webhookToleranceSeconds: 0 }, )).resolves.toMatchObject({ type: 'checkout.session.completed' }); }); + + it('falls back to the default timestamp tolerance for invalid config values', async () => { + const raw = JSON.stringify({ type: 'checkout.session.completed' }); + const secret = 'whsec_test'; + const timestamp = String(Math.floor(Date.now() / 1000) - 600); + const signature = createHmac('sha256', secret) + .update(`${timestamp}.${raw}`) + .digest('hex'); + + await expect(adapter.verifyWebhook( + ctx({ STRIPE_WEBHOOK_SECRET: secret }), + raw, + `t=${timestamp},v1=${signature}`, + { webhookToleranceSeconds: Number.NaN }, + )).rejects.toThrow('Stripe webhook signature timestamp outside tolerance'); + }); }); function ctx(secrets: Record) { diff --git a/packages/payments/stripe/src/index.ts b/packages/payments/stripe/src/index.ts index 62694777..e69073a4 100644 --- a/packages/payments/stripe/src/index.ts +++ b/packages/payments/stripe/src/index.ts @@ -168,6 +168,8 @@ function verifyStripeSignature( secret: string, toleranceSeconds = 300, ): void { + const effectiveToleranceSeconds = normalizeWebhookToleranceSeconds(toleranceSeconds); + // Parse all key=value pairs preserving duplicate v1 keys. // Stripe sends multiple v1 signatures during webhook secret rotation; // Object.fromEntries() would silently drop all but one. @@ -192,9 +194,9 @@ function verifyStripeSignature( const timestampSeconds = Number(timestamp); if (!Number.isFinite(timestampSeconds)) throw new Error('Stripe-Signature timestamp is invalid'); - if (toleranceSeconds > 0) { + if (effectiveToleranceSeconds > 0) { const age = Math.floor(Date.now() / 1000) - timestampSeconds; - if (Math.abs(age) > toleranceSeconds) throw new Error('Stripe webhook signature timestamp outside tolerance'); + if (Math.abs(age) > effectiveToleranceSeconds) throw new Error('Stripe webhook signature timestamp outside tolerance'); } const expected = createHmac('sha256', secret) @@ -217,6 +219,12 @@ function verifyStripeSignature( } } +function normalizeWebhookToleranceSeconds(value: number): number { + if (value === 0) return 0; + if (Number.isFinite(value) && value > 0) return value; + return 300; +} + function normalizeStripeStatus(status: unknown): Webhook['status'] | undefined { if (status === 'paid' || status === 'complete' || status === 'succeeded') return 'succeeded'; if (status === 'open' || status === 'processing' || status === 'requires_payment_method') return 'pending';