Home/Blog/Payment Security
Payment SecurityPublished · Updated ⚡ 10 min read

How AI Code Leaks Stripe Money: Webhook Forgery, Key Exposure & The $87,000 Forensic Post-Mortem

A 10-minute deep forensic investigation into how AI coding tools leak Stripe secret keys, bypass webhook signature verification, and expose SaaS startups to massive financial fraud.

VG
Vladyslav Gusarov
DevSecOps Lead at Bryxe
How AI-Generated Code Leaks Stripe Money: Webhook Forgery & $87,000 Post-Mortem

Last month, a solo developer building an AI workflow automation tool using Cursor pushed a feature commit to a public GitHub repository. Within 28 seconds, an automated botnet monitoring the GitHub public events stream scraped the commit, extracted a live Stripe secret key (sk_live_...), and began executing card-testing attacks against the merchant account.

By 07:00 AM, the founder discovered 43,000 unauthorized transaction attempts, $87,000 in dispute liability and merchant fees, and an immediate suspension of their payment processing infrastructure.

Payment integrations are the single highest-liability surface in any commercial application. Yet AI coding models routinely generate payment architectures that are fundamentally insecure.

Here is the engineering post-mortem of how AI code leaks payment credentials, how attackers forge webhook payloads, and the exact production patterns required to secure your billing stack.


1. The Anatomy of a 28-Second Secret Key Exfiltration

Public GitHub commits are ingested in real time by automated scanning engines run by both legitimate security firms and malicious actors.

When developers ask AI to build a payment service, the model often outputs a functional template with inline strings:

typescriptSource Code
// lib/stripe.ts generated by AI
import Stripe from 'stripe';

export const stripe = new Stripe('sk_live_51Pxxxxxxxxxxxxxxxxxxxxxx', {
  apiVersion: '2024-06-20',
});

The developer plans to replace the string with process.env.STRIPE_SECRET_KEY before deployment, but accidentally stages the file via git commit -am "add stripe checkout".

Even if you notice the mistake 2 minutes later and push a commit removing the key, the secret remains permanently preserved in your git commit history. Attackers clone the raw commit tree and extract the key immediately.

Emergency Key Invalidation Runbook:

  1. Immediately log into dashboard.stripe.com/apikeys and Revoke the compromised key.
  2. Generate a new secret key with restricted permissions where possible.
  3. Purge the commit history using git filter-repo or BFG Repo-Cleaner before force-pushing.

2. Forgeable Webhooks: Skipping constructEvent

This is the most widespread vulnerability uncovered in AI-generated billing code.

When a customer completes a purchase, Stripe dispatches an asynchronous HTTP POST event to your webhook URL (/api/webhooks/stripe). Because reading raw request streams in modern server runtimes (Next.js App Router, Cloudflare Workers, Fastify) requires handling unparsed body buffers, AI models frequently take the path of least resistance: parsing the body as standard JSON.

The Insecure AI Implementation:

typescriptSource Code
// app/api/webhooks/stripe/route.ts
export async function POST(req: Request) {
  // FATAL FLAW: Parses body directly as JSON without validating Stripe signature
  const event = await req.json();

  if (event.type === 'checkout.session.completed') {
    const customerId = event.data.object.customer;
    await activateSubscriptionInDatabase(customerId);
  }

  return new Response('Webhook received', { status: 200 });
}

The Attacker Exploit:

An attacker does not need to enter a credit card or initiate a checkout session. They send a forged curl request directly to your webhook endpoint:

bashSource Code
curl -X POST https://yourapp.com/api/webhooks/stripe \
  -H "Content-Type: application/json" \
  -d '{
    "type": "checkout.session.completed",
    "data": {
      "object": {
        "customer": "cus_attacker_user_id"
      }
    }
  }'

Your server reads the payload, finds type === 'checkout.session.completed', and immediately grants Enterprise permissions to the attacker for free.

The Secure Production Implementation:

You must extract the unparsed raw text buffer and verify the cryptographic signature against your STRIPE_WEBHOOK_SECRET:

typescriptSource Code
// app/api/webhooks/stripe/route.ts
import { headers } from 'next/headers';
import { stripe } from '@/lib/stripe';

export async function POST(req: Request) {
  const body = await req.text(); // Raw unparsed body
  const signature = (await headers()).get('stripe-signature');

  if (!signature) {
    return new Response('Missing stripe-signature header', { status: 400 });
  }

  let event;
  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err) {
    console.error('Webhook signature verification failed:', (err as Error).message);
    return new Response('Invalid webhook signature', { status: 400 });
  }

  if (event.type === 'checkout.session.completed') {
    const session = event.data.object;
    await activateSubscriptionInDatabase(session.customer as string);
  }

  return new Response('Webhook verified', { status: 200 });
}

3. Webhook Idempotency & Replay Attack Vulnerabilities

Stripe guarantees at-least-once delivery for webhooks. If your server experiences latency or network hiccups, Stripe will retry sending the same webhook event multiple times.

AI models rarely implement idempotency checks:

typescriptSource Code
if (event.type === 'invoice.payment_succeeded') {
  // Vulnerable: If received 3 times, grants 300 credits instead of 100
  await addCreditsToUser(userId, 100);
}

The Production Fix:

Store processed Stripe Event IDs in an idempotent ledger table inside a database transaction:

typescriptSource Code
await prisma.$transaction(async (tx) => {
  const existing = await tx.processedWebhookEvent.findUnique({
    where: { eventId: event.id },
  });
  if (existing) return; // Already processed

  await tx.processedWebhookEvent.create({
    data: { eventId: event.id, processedAt: new Date() },
  });

  await tx.user.update({
    where: { id: userId },
    data: { credits: { increment: 100 } },
  });
});

4. Client-Controlled Price Manipulation

When asked to generate a billing page, AI frequently places the Stripe Price ID on client-side UI buttons:

tsxSource Code
<button onClick={() => checkout('price_1NxEnterprisePlan')}>
  Upgrade to Enterprise ($499/mo)
</button>

The backend then accepts req.body.priceId without validation. An adversary intercepts the API request, changes the price ID to a $1 test price, and completes checkout.

Rule: The client must only transmit an abstract plan identifier (plan: "pro"). The server resolves the price ID from an environment-secured dictionary.


Run the 60-Second Stripe Safety Audit

Do not wait until a compromised key drains your bank account or results in merchant processor termination.

Bryxe Shield provides a specialized Stripe Safety Audit tool: - Scans git commits and code for active sk_live_ and webhook secrets. - Analyzes every webhook handler for constructEvent signature enforcement. - Flags client-controlled price tampering vectors. - Tests production JavaScript bundles for leaked environment variables.

Run your Stripe Safety Audit now at bryxe.app/stripe-audit.

AUTOMATED DEFENSE

Don't wait for an exploit to audit your codebase

Review supported code risks, exposed secrets and dependency findings with Bryxe Shield. Verify the fixes in your application before release.

Need a practical next step? Explore the security field guides or read our editorial and sourcing policy.

Recommended Security Research