A webhook is just a URL on your server that a service like Stripe calls when something happens, a payment, a subscription change, a new signup. Because it is a public URL, anyone on the internet can send a POST to it, not only the real service. So the question your handler has to answer is: did this event actually come from the service, or did someone forge it? An AI-built handler usually does not ask, and that is the problem.
What goes wrong
The generated handler reads the event and acts on it, granting access, marking an order paid, crediting an account, without checking that the request is genuine. Since the payload is entirely attacker-controlled, anyone can send a fake event:
// the vulnerable pattern: trust the payload, no verification
app.post("/webhooks/stripe", async (req, res) => {
const event = req.body;
if (event.type === "checkout.session.completed") {
await grantAccess(event.data.object.customer); // anyone can POST this
}
res.sendStatus(200);
});A single crafted request that looks like a completed checkout gets your app to grant access with no payment. The endpoint trusted the event because nothing told it not to.
Want to see exactly what Prbl flags? Watch it scan a demo app, no repo or account needed.
See a live scan →The fix: verify the signature
The service signs each request with a secret only you and it share, and includes the signature in a header. You verify it against the raw request body before trusting the event. With Stripe, the library does the check for you:
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
app.post("/webhooks/stripe",
express.raw({ type: "application/json" }), // raw body, not parsed JSON
async (req, res) => {
let event;
try {
event = stripe.webhooks.constructEvent(
req.body, // the raw bytes
req.headers["stripe-signature"],
process.env.STRIPE_WEBHOOK_SECRET, // the signing secret
);
} catch {
return res.sendStatus(400); // bad signature -> reject
}
if (event.type === "checkout.session.completed") {
await grantAccess(event.data.object.customer);
}
res.sendStatus(200);
},
);Now a forged request fails the signature check and is rejected before your code acts on it. Two details matter: verify against the raw body (parsing it to JSON first breaks the signature), and keep the signing secret server-side like any other secret. The same idea applies to any provider that signs its webhooks; check their docs for the exact header and method.
Frequently asked questions
What is a webhook signature?
When a service like Stripe sends your app a webhook, it signs the request with a secret only you and the service share, and puts that signature in a header. Verifying it means recomputing the signature from the raw request body and your signing secret and checking it matches. If it does, the event genuinely came from the service and was not altered. If it does not, someone is forging or tampering with the request.
Why is an unverified webhook dangerous?
Because your webhook endpoint is a public URL that anyone can send a POST to. If you act on the event without verifying the signature, an attacker can send a fake 'payment succeeded' or 'subscription active' event and get your app to grant access or credit an account without paying. The endpoint trusts the payload, and the payload is attacker-controlled. Verification is what ties the event back to the real service.
Why do AI tools skip it?
Because the endpoint works without it. The assistant wires up a route that parses the event and updates your database, and in testing the events come from the real service so everything behaves. Nothing in the happy path requires the signature check, so it gets left out. The gap only appears when someone sends a forged request, which never happens during development.
Do I need the raw request body to verify?
Usually yes, and this trips people up. Signature verification is computed over the exact bytes of the request body, so if your framework parses the body to JSON before you verify, the signature will not match. You need to read the raw body for the verification step, then parse it after the signature checks out. Most webhook libraries document the raw-body requirement for exactly this reason.
Check your webhook handlers
Any endpoint that receives events from a third party and acts on them is worth a look. It fits into the broader pass of securing an AI-built API, covered here, and a scan can flag a handler that acts on unverified input. Run a free scan and see whether your app trusts events it should be verifying.