From bb1fb8c8ef12508f133ad61cb20a815d0095583e Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Sat, 30 May 2026 03:20:42 -0600 Subject: [PATCH 1/3] fix(stripe): accept any matching webhook signature --- packages/payments/stripe/src/index.ts | 29 ++++++++++++++++----------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/payments/stripe/src/index.ts b/packages/payments/stripe/src/index.ts index 38e7a890..4f065c9a 100644 --- a/packages/payments/stripe/src/index.ts +++ b/packages/payments/stripe/src/index.ts @@ -162,25 +162,30 @@ async function readStripeJson(res: Response): Promise { } function verifyStripeSignature(rawBody: string, header: string, secret: string): void { - const parts = Object.fromEntries( - header.split(',').map((part) => { - const [key, value] = part.split('='); - return [key, value]; - }), - ); - const timestamp = parts.t; - const signature = parts.v1; - if (!timestamp || !signature) throw new Error('Stripe-Signature missing t or v1'); + let timestamp: string | undefined; + const signatures: string[] = []; + + for (const part of header.split(',')) { + const [key, value] = part.split('='); + if (key === 't') timestamp = value; + if (key === 'v1' && value) signatures.push(value); + } + + if (!timestamp || signatures.length === 0) throw new Error('Stripe-Signature missing t or v1'); const expected = createHmac('sha256', secret) .update(`${timestamp}.${rawBody}`) .digest('hex'); - const actualBuffer = Buffer.from(signature, 'hex'); const expectedBuffer = Buffer.from(expected, 'hex'); - if (actualBuffer.length !== expectedBuffer.length || !timingSafeEqual(actualBuffer, expectedBuffer)) { - throw new Error('Invalid Stripe webhook signature'); + for (const signature of signatures) { + const actualBuffer = Buffer.from(signature, 'hex'); + if (actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer)) { + return; + } } + + throw new Error('Invalid Stripe webhook signature'); } function normalizeStripeStatus(status: unknown): Webhook['status'] | undefined { From dcf3dea8957303d999f6106c267577ae3bd18095 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Sat, 30 May 2026 03:20:42 -0600 Subject: [PATCH 2/3] test(stripe): cover multiple webhook signatures --- packages/payments/stripe/src/index.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/payments/stripe/src/index.test.ts b/packages/payments/stripe/src/index.test.ts index b25da9e1..a93910db 100644 --- a/packages/payments/stripe/src/index.test.ts +++ b/packages/payments/stripe/src/index.test.ts @@ -96,6 +96,24 @@ describe('payment-stripe', () => { }); }); + it('accepts any matching Stripe v1 signature when multiple are present', async () => { + const raw = JSON.stringify({ type: 'checkout.session.completed' }); + const secret = 'whsec_test'; + const timestamp = '1700000000'; + const signature = createHmac('sha256', secret) + .update(`${timestamp}.${raw}`) + .digest('hex'); + + const webhook = await adapter.verifyWebhook( + ctx({ STRIPE_WEBHOOK_SECRET: secret }), + raw, + `t=${timestamp},v1=${signature},v1=${'0'.repeat(64)}`, + {}, + ); + + expect(webhook.type).toBe('checkout.session.completed'); + }); + it('rejects invalid Stripe webhook signatures', async () => { await expect(adapter.verifyWebhook( ctx({ STRIPE_WEBHOOK_SECRET: 'whsec_test' }), From 7eff0841c70f922a9b904816e41c6a71c123475f Mon Sep 17 00:00:00 2001 From: jsdavid278-cyber Date: Sat, 30 May 2026 16:35:20 -0600 Subject: [PATCH 3/3] test(stripe): cover matching signature last --- packages/payments/stripe/src/index.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/payments/stripe/src/index.test.ts b/packages/payments/stripe/src/index.test.ts index a93910db..44c15a64 100644 --- a/packages/payments/stripe/src/index.test.ts +++ b/packages/payments/stripe/src/index.test.ts @@ -112,6 +112,15 @@ describe('payment-stripe', () => { ); expect(webhook.type).toBe('checkout.session.completed'); + + const webhookWithMatchingSignatureLast = await adapter.verifyWebhook( + ctx({ STRIPE_WEBHOOK_SECRET: secret }), + raw, + `t=${timestamp},v1=${'0'.repeat(64)},v1=${signature}`, + {}, + ); + + expect(webhookWithMatchingSignatureLast.type).toBe('checkout.session.completed'); }); it('rejects invalid Stripe webhook signatures', async () => {