The $10,000 Stripe Webhook Bug Hiding in AI-Generated Code
A walkthrough of a Stripe webhook vulnerability that shows up in AI-generated code — and the few lines that fix it.
I've been building a security scanner for AI-generated code, which means I spend a lot of time reading what Cursor, Bolt, and Lovable actually produce when you ask them to “add Stripe payments.” And there's one specific bug I keep finding: the Stripe webhook endpoint doesn't verify signatures.
On its own, that sounds like a minor configuration issue. In practice, it's a one-line mistake that lets any attacker mark any order as paid — without ever sending a single dollar to Stripe.
If you're shipping a product built with an AI coding assistant and it takes Stripe payments, check for this bug before you do anything else. Here's exactly how it works, how to find it, and how to fix it.
The Scenario
Imagine a SaaS app that sells API credits. Users pay through Stripe Checkout, and when the payment succeeds, a webhook fires to your backend. Your backend receives the webhook, sees “payment succeeded,” and credits the user's account.
Pretty standard flow. Here's the kind of webhook handler AI assistants commonly generate:
// server.js
app.post("/api/webhooks/stripe", express.json(), async (req, res) => {
const event = req.body;
if (event.type === "checkout.session.completed") {
const session = event.data.object;
const userId = session.metadata.userId;
const credits = parseInt(session.metadata.credits);
await db.users.update({
where: { id: userId },
data: { credits: { increment: credits } },
});
}
res.json({ received: true });
});Looks reasonable. It handles the event, pulls out the metadata, credits the user. Tests pass. Stripe dashboard shows successful payments. Ship it.
Except this endpoint is completely open. Any attacker who knows the URL can send a fake “payment succeeded” event and get unlimited free credits.
How the Attack Works
The attacker writes a single curl command:
curl -X POST https://yourapp.com/api/webhooks/stripe \
-H "Content-Type: application/json" \
-d '{
"type": "checkout.session.completed",
"data": {
"object": {
"metadata": {
"userId": "attacker-user-id",
"credits": "10000"
}
}
}
}'That's it. Your server receives the request, sees a “payment succeeded” event, and credits the attacker's account with 10,000 API credits. No payment ever happened. Stripe was never involved. You just gave away your product.
If your credits cost $1 each, that's $10,000 in revenue vanishing into a terminal.
The scary part: this bug is invisible in every normal test. Real Stripe payments work perfectly. Your Stripe dashboard shows correct revenue numbers. The only way you find out is when you notice your margins cratering, or when someone on Reddit writes “thanks for the free credits lol.”
Why AI Coding Tools Generate This
AI coding tools can produce variations of this code because it's a common pattern in tutorials and Stack Overflow answers. The training data is full of tutorials and Stack Overflow answers that show the “happy path” — receive webhook, parse body, update database. They almost never include the security check, because security checks make the example longer and harder to read.
So when you ask an AI assistant to “add Stripe payment handling,” you can get the version without the check. The code runs. The payments process. The AI moves on to the next task.
Unless you know to ask, you never see the missing line.
The Fix (A Few Lines)
Stripe actually provides a library function that makes this trivial to fix. The fix takes a handful of lines:
// server.js
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
app.post(
"/api/webhooks/stripe",
express.raw({ type: "application/json" }), // changed from express.json()
async (req, res) => {
const sig = req.headers["stripe-signature"];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
webhookSecret
);
} catch (err) {
return res.status(400).send(`Webhook error: ${err.message}`);
}
if (event.type === "checkout.session.completed") {
const session = event.data.object;
const userId = session.metadata.userId;
const credits = parseInt(session.metadata.credits);
await db.users.update({
where: { id: userId },
data: { credits: { increment: credits } },
});
}
res.json({ received: true });
}
);What this does:
express.raw()instead ofexpress.json()— Stripe needs the raw request body to verify the signature. If Express parses it first, the verification fails.- Read the
stripe-signatureheader — Stripe signs every webhook with a secret you get from your dashboard. Real webhooks have this signature. Fake ones don't. stripe.webhooks.constructEvent()— This function cryptographically verifies the signature matches the body and the secret. If it doesn't match, it throws.- Return 400 on failure — If verification fails, reject the request. Don't even look at the body.
Attackers can't fake this. They don't have your webhook secret, and they can't compute a valid signature without it. Their curl commands now get a 400 error instead of free credits.
How to Check Your Own Code
Open your project and search for stripe/webhook or webhooks/stripe or any file that handles Stripe events. Look at the handler. If you see any of these, you have the vulnerability:
const event = req.body;without signature verificationJSON.parse(req.body)without verificationexpress.json()middleware on a Stripe webhook route- Any code that reads
event.typebefore callingconstructEvent()
If you see stripe.webhooks.constructEvent(...) on the raw body, wrapped in a try/catch that returns an error on failure, the signature check is in place.
Other AI-Generated Vulns That Look Like This
Stripe webhooks are the scariest example because the impact is immediate financial loss, but this same pattern — “the AI skipped the security check because it wasn't in the happy path” — shows up everywhere in AI-generated code:
- Clerk/Auth0 webhooks without signature verification → attackers can create users or modify sessions
- GitHub webhooks without signature verification → attackers can trigger fake push events
- Resend/SendGrid webhooks without signature verification → attackers can mark emails as delivered
- SQL queries built with string concatenation → attackers can dump your entire database
- API routes without auth middleware → attackers can hit admin endpoints
dangerouslySetInnerHTMLwith user input → attackers can inject scripts
Every single one of these has a “correct” version that's one or two extra lines of code. Every single one is the thing the AI skipped.
What I've Been Doing About It
I kept seeing the same pattern in AI-generated codebases, so I built XploitScan — a security scanner with 223 rules specifically written for the mistakes AI coding tools make. It catches the Stripe webhook bug, the missing auth middleware, the SQL injection, and 200+ other patterns. It's free to try — paste a snippet or upload a file with no signup, or create a free account to upload a whole project (5 scans a day).
If you've built something with Cursor, Bolt, or Lovable in the last few months, you should probably scan it. The webhook bug alone is worth the 30 seconds.
But honestly, the more important thing is just: know the pattern. AI tools ship the happy path. If a service integration uses webhooks, signatures, or tokens, ask the AI explicitly to “add signature verification” — don't assume it did. And never trust a webhook body without verifying it first.
Your future self (and your margins) will thank you.
Check your code in 30 seconds
Paste a snippet or upload a file. Free. No signup.
Scan Your Code — Free