From 6b109239cfc893959eb9ccb75bfc95f694afae2c Mon Sep 17 00:00:00 2001 From: anupamme Date: Sun, 6 Sep 2026 01:45:17 +0000 Subject: [PATCH] fix: V-001 security vulnerability Automated security fix generated by OrbisAI Security --- server/src/routes/webhook.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/server/src/routes/webhook.js b/server/src/routes/webhook.js index 08f35b0..71bc568 100644 --- a/server/src/routes/webhook.js +++ b/server/src/routes/webhook.js @@ -10,6 +10,9 @@ const storage = require('../storage'); const router = express.Router(); +// Maximum age of a webhook event before it is rejected as a potential replay. +const WEBHOOK_EVENT_MAX_AGE_SECONDS = 300; + async function getCustomerEmail(customerId) { const customer = await stripe.customers.retrieve(customerId); return customer.email ? customer.email.toLowerCase() : null; @@ -33,6 +36,14 @@ router.post('/stripe', async (req, res) => { return res.status(400).json({ error: 'Invalid signature' }); } + // Reject stale events to mitigate replay attacks (an intercepted, validly + // signed event replayed later than when Stripe originally sent it). + const eventAgeSeconds = Math.floor(Date.now() / 1000) - event.created; + if (eventAgeSeconds > WEBHOOK_EVENT_MAX_AGE_SECONDS) { + console.error(`[webhook] Rejected stale event ${event.id} (age: ${eventAgeSeconds}s)`); + return res.status(400).json({ error: 'Event too old' }); + } + // Reject events whose product isn't in our allowlist. Protects against // cross-product bleed when multiple apps share a Stripe account. try {