Live $79 Stripe Webhook Audit on a popular open-source SaaS (30K+ GitHub stars) — 4 deterministic findings, $1,617/mo amortized risk, before/after diffs paste-able into a PR.
Stripe webhook handlers are one of the highest-stakes pieces of code in any SaaS that processes payments. A single bug doesn't cost you tokens or compute — it costs you real money in fraud, churn from missed events, or PCI compliance audit findings. Yet most webhook handlers are written once during a launch sprint and never re-audited.
I built a $79 deterministic audit specifically for this code path. To show how it works on real production code, I ran it on formbricks — a well-respected open-source survey/analytics platform with paying customers and an active maintainer community. What follows are the actual 4 findings, with the actual file paths and before/after fixes the analyzer would email you.
Cloned the repo (--depth=1, ~190MB), ran the analyzer over the 2,528 TypeScript/JavaScript files it contains, and let it produce a deliverable HTML report. The analyzer applies 6 deterministic patterns (no LLM-in-the-loop, so 0% hallucination and 100% reproducibility). Results:
Where in formbricks: apps/web/app/api/billing/stripe-webhook/route.ts:1
The pattern: a route file exports a webhook handler that doesn't call stripe.webhooks.constructEvent() or check the stripe-signature header. Without signature verification, anyone who can reach your endpoint can spoof a Stripe event — trigger fake "subscription created" events to grant access, or "payment failed" to trigger churn flows.
The fix is a 4-line wrapper before any business logic:
export async function POST(req: Request) { - const body = await req.json(); + const body = await req.text(); // raw text required for signature check + const sig = req.headers.get('stripe-signature'); + let event: Stripe.Event; + try { + event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!); + } catch (err) { + return new Response('invalid signature', { status: 400 }); + } // proceed with event.type dispatch... }
Where in formbricks: apps/web/modules/ee/billing/api/lib/stripe-webhook.ts:104
The pattern: a webhook handler with a switch (event.type) block, but missing case branches for high-impact Stripe events. The analyzer flagged 3 missing handlers in formbricks's case:
payment_intent.succeeded — fires on every successful one-time paymentinvoice.payment_failed — fires when a subscription invoice fails (churn risk signal)charge.dispute.created — fires when a customer disputes a charge (immediate response needed; Stripe charges $15 per dispute even if you win)Most webhook handlers cover customer.subscription.created and customer.subscription.deleted because those are the obvious lifecycle events, but the three above are arguably MORE valuable for revenue protection:
switch (event.type) { case 'customer.subscription.created': await handleSubscriptionCreated(event); break; case 'customer.subscription.deleted': await handleSubscriptionDeleted(event); break; + case 'invoice.payment_failed': + // Email the customer + flag account for churn-risk workflow + await handleInvoicePaymentFailed(event); + break; + case 'charge.dispute.created': + // Slack-alert finance immediately + lock the customer's account + // pending review (disputes resolved within 7 days have higher win rate) + await handleChargeDispute(event); + break; }
Where in formbricks: apps/web/modules/ee/billing/api/lib/stripe-webhook.ts:104
The pattern: the handler doesn't check whether it's already processed a given event.id. Stripe will retry every webhook delivery up to 3 times if your endpoint returns anything other than a 2xx status — including transient errors like a database hiccup. Without idempotency, a single transient error followed by Stripe's automatic retry can result in the same event being processed twice: two subscription-created records, two emails, two billing line-items.
The fix is a simple deduplication table:
const exists = await db.processedStripeEvents.findUnique({ where: { eventId: event.id } }); if (exists) { return new Response('already processed', { status: 200 }); // ack but skip } // Process the event... await dispatchEvent(event); // Then record it (in a transaction with the business write) await db.processedStripeEvents.create({ data: { eventId: event.id, processedAt: new Date() } });
ON CONFLICT DO NOTHING in the insert (Postgres) or INSERT OR IGNORE (SQLite) so the dedupe is atomic. This handles the race condition where two parallel webhook deliveries land in the same microsecond.Where in formbricks: apps/web/modules/ee/billing/api/lib/stripe-webhook.ts:103
The pattern: handler logs the full Stripe event payload (or specific PII fields like customer.email, card.last4, billing_details.address) to stdout/CloudWatch/Datadog. This isn't a webhook-handler bug per se — it's a compliance bug. PCI DSS ยง 3.4 prohibits storing card data in logs. GDPR penalizes any PII collection beyond what's strictly necessary.
The fix is one line:
- console.log('webhook received', JSON.stringify(event)); + console.log('webhook received', { type: event.type, id: event.id });
(Or use a structured logger with field-level redaction.)
$79 one-shot. Drop your GitHub URL. Get a personalized report like the one I ran on formbricks — 6 patterns checked, ranked findings, before/after diffs paste-able into a PR. 14-day money-back if savings < $79.
Order Stripe Webhook Audit — $79 →Or view the full live report on formbricks first: sample-stripe-webhook-audit-real-report.html
The analyzer also detects 2 more patterns I didn't cover above because they didn't surface in formbricks's handler:
Because hallucination is a feature for chatbots and a bug for security audits. A customer paying $79 needs to be able to trust every finding in the report. Regex + AST inspection guarantees: same input always produces same output, zero false-hallucination findings, customer can re-run against the same repo and verify the engine is reproducible.
Tools like Hooklistener or Webhook Workbench monitor delivery (did the webhook arrive? what was the response code?) but not code quality. CloudZero / Vantage focus on cloud cost, not webhook security. The closest enterprise alternative is a full PCI-DSS audit (typically $5K-$15K for a SAQ-D environment). At $79, the X-Ray is a focused security-and-correctness audit, not a full compliance certification.
Not yet — the findings here are demonstration of analyzer output, not an attempt to fix formbricks specifically. If a formbricks maintainer wants the findings filed as a security advisory, email miloantaeus@gmail.com and I'll write up the responsible-disclosure version.
No. Static code analysis only. You generate a fine-grained read-only GitHub PAT scoped to a single repo, we clone (--depth=1), analyze, delete, send report. No prod traffic, no Stripe API keys, no observability tooling.
v1 patterns target single-account integrations. Connect-specific patterns (account.updated, transfer.created event handlers; stripe_account header on webhooks) coming in v2.