Skip to content
Chapter 05 · 10 min

Exposed endpoints, APIs, and webhooks

Your app's attack surface is every request it will answer, not only the screens you can click to. AI tools leave behind endpoints you forgot you shipped, APIs that ask for no credentials, and webhooks that trust anyone who POSTs to them. This chapter is the surface you can't see in the UI, and how to close it.

Renovating the storefront but leaving the old back door unlocked, and off the floor plan.

The endpoints you forgot you shipped

Changing the UI doesn't delete the API behind it. You remove a button, the screen looks clean, and the route that button called is still live and still answering. Across iterations an AI-built app accumulates these: old versions of endpoints, debug and test routes added "just to check something," admin actions that were never gated. They're invisible in the app and fully reachable by anyone who knows, or guesses, the URL.

The mental shift: your security boundary is the set of routes your server will respond to, not the set of links in your interface. An attacker reads your JavaScript bundle to enumerate the routes, ignores your UI entirely, and calls them directly. Removed-from-the-screen is not removed-from-the-server.

Webhooks: anyone can POST that

A webhook is a public URL on your server that a provider (Stripe, GitHub, a mail service) calls to tell you something happened. The catch: the URL is public, so anyone can call it, including an attacker sending a forged "payment succeeded" or "subscription upgraded" to unlock paid features for free. The provider signs every real call with a shared secret; if you don't verify that signature, you're trusting strangers' claims about your own business.

// Verify the signature BEFORE trusting anything in the payload.
const sig = req.headers.get("stripe-signature");
let event;
try {
  event = stripe.webhooks.constructEvent(rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET);
} catch {
  return new Response("bad signature", { status: 400 }); // forged or tampered
}
// Only now is it safe to act on event.type

CORS and rate limits

Two settings AI tools get wrong because the permissive version makes errors go away. CORS controls which other websites' scripts may call your API from a browser; a tool stuck on a cross-origin error will often set it to allow everything, which, combined with credentials, can let any site act as your user. Lock it to your own origins. And rate limiting: without it, login and signup are open to brute force, expensive endpoints are open to bill-running abuse, and your data is open to scraping.

  • CORS: allow only your real origins, never '*' together with credentials.
  • Rate-limit by user and by IP, especially on auth, signup, password reset, and anything that costs money to run.
  • Put expensive or sensitive endpoints behind authentication first; rate limiting is a backstop, not the front line.

The inventory you need

You can't secure a surface you haven't mapped. Once, before launch, enumerate it:

  • List every route the server actually serves, from the code and the bundle, not from the menu.
  • For each: is it meant to be public? If not, does it authenticate and authorize (chapter 4)?
  • Delete dead, debug, and old-version routes; the smallest surface is the safest.
  • For every inbound webhook, confirm signature verification, then confirm it rejects an unsigned call.

This inventory is also the input to the launch checklist in the next chapter. You can't tick "every endpoint is authorized" until you know what every endpoint is.

In one line each

  • Your attack surface is every route the server answers, not every link in the UI; removed-from-screen isn't removed-from-server.
  • Webhook URLs are public; without signature verification, anyone can forge events like 'payment succeeded'.
  • CORS set to '*' and missing rate limits are permissive defaults AI tools reach for to silence errors; both are holes.
  • Inventory every endpoint before launch: is it meant to be public, is it authorized, can dead routes be deleted, are webhooks verified?