The $10,000 Stripe Webhook Bug Hiding in AI-Generated Code
A walkthrough of the Stripe webhook vulnerability that Cursor, Bolt, and Lovable ship by default — and the 4 lines of code 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 find in almost every single project: 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, there's a better-than-even chance you have this exact bug in your app right now. 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 code Cursor generated for the webhook handler:
// 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
Cursor, Bolt, Lovable, and Replit all produce variations of this code because it's the most common webhook handler pattern in their training data. 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 Cursor to “add Stripe payment handling,” you 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 (4 Lines)
Stripe actually provides a library function that makes this trivial to fix. The fix is exactly 4 lines of code:
// 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(...) wrapped in a try/catch, you're safe.
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 131 rules specifically written for the mistakes AI coding tools make. It catches the Stripe webhook bug, the missing auth middleware, the SQL injection, and 128 other patterns. It's free to try — drag and drop your project, no signup, 5 free scans per 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.