Your app trusts a lot of things it did not write. A package from npm. A webhook from Stripe. An update that auto-installs from a CDN. Each of those is a pipe into your server. The question nobody asks is whether the pipe has a valve on it.
Software and data integrity failures happen when your app trusts code or data it cannot verify came from the right place. Unsigned updates, unverified webhooks, insecure deserialization, CI pipelines that install whatever the registry serves. The pattern is always the same — a channel you trusted turns out to have no lock on it.
What your AI actually built
You asked for a Stripe webhook handler, and the model wrote one. It parses the JSON body, reads the event type, marks the order as paid, and sends the receipt. It works the first time you test it. You ship it.
What it skipped was the signature check. Stripe signs every webhook with a secret only you and Stripe share — and the handler is supposed to verify that signature before trusting a single byte. Without it, anyone who finds the endpoint can POST their own fake 'payment succeeded' event.
The same shape appears everywhere. A deploy script that curls a tarball and runs it unsigned. A plugin loader that eval's remote JavaScript. A CI step that installs from a typo-squatted package. The code does what you asked. It just trusts an input it has no business trusting.
How it gets exploited
The attacker finds your Stripe webhook URL in a network tab or a leaked .env. The handler is live.
- 1Read the docsThey look up the exact JSON shape of a Stripe 'checkout.session.completed' event in the public documentation.
- 2Forge a payloadThey build a fake event with an order ID from your public order flow and the amount set to whatever they want.
- 3POST it coldThey send the fake event directly to your webhook endpoint. No signature header, no Stripe involvement — just a raw POST.
- 4Free orders foreverYour handler parses the JSON, marks the order paid, and ships the product. The real Stripe dashboard shows nothing because the real Stripe never saw the request.
The attacker places unlimited orders at zero cost until someone manually reconciles the ledger. By then the inventory is gone.
Vulnerable vs Fixed
// app/api/webhooks/stripe/route.ts
export async function POST(req) {
const event = await req.json();
if (event.type === 'checkout.session.completed') {
await db.order.update({
where: { id: event.data.object.metadata.orderId },
data: { status: 'paid' },
});
}
return Response.json({ received: true });
}// app/api/webhooks/stripe/route.ts
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET!);
export async function POST(req) {
const body = await req.text();
const sig = req.headers.get('stripe-signature')!;
let event;
try {
event = stripe.webhooks.constructEvent(
body,
sig,
process.env.STRIPE_WEBHOOK_SECRET!,
);
} catch {
return Response.json({ error: 'Bad signature' }, { status: 400 });
}
if (event.type === 'checkout.session.completed') {
await db.order.update({
where: { id: event.data.object.metadata.orderId },
data: { status: 'paid' },
});
}
return Response.json({ received: true });
}The fix is one call — constructEvent — that verifies the signature against the shared secret before the handler trusts anything. If the signature is missing or wrong, the request dies at the door.
A real case
SolarWinds shipped a signed update that contained a backdoor
In 2020 attackers planted malicious code inside a SolarWinds build pipeline, so the company's own signed update went out to 18,000 customers including US federal agencies.
Read the case studyRelated reading
References
Check who your app is actually trusting.
Flowpatrol probes every webhook and integration endpoint for missing signature checks. Five minutes. One URL.
Try it free