Vibe Audit Blog

← Blog

A field guide to the holes AI leaves in payment sites

July 16, 2026

AI coding tools are good at the path where everyone behaves. A real customer pays with a real card, gets a real receipt, uploads a real file, and the operator downloads it. That path they build well.

The branch they skip is the one that assumes the caller is lying. On anything that takes money or files, that branch is the whole game — and it's where nearly every serious hole lives. One study of AI-generated code found that close to half of the samples shipped a known vulnerability, and the OWASP project still ranks broken access control — the "did anyone check this caller is allowed?" failure — as the number one web risk. AI writes code that runs, not code that refuses.

Below is the surface, category by category. It looks like a dozen different bugs. It's mostly one bug: the app trusts what the caller sent it. I've grouped it by what gets trusted, because that's the fastest way to check your own app.

Group 1: trusting who's calling

The app takes the caller's word for their identity instead of proving it.

The webhook trusts "I'm Stripe." The generated handler checks that a signature header is present, then parses the event without checking the signature is real. Anyone can POST a fake "payment succeeded," with any email attached, and the site sends a confirmation from the real domain. Worse, without an idempotency check the same event replayed ten times fulfills the order ten times.

The upload (or any "paid users only") endpoint trusts "the form says I paid." It never checks back with Stripe. Send a form with any email and it treats you as a paying customer. Identity has to come from the payment session on the server, never from the request body.

The session token is forgeable or immortal. JWTs signed with a weak or default secret, or with no expiry and no rotation, mean one leaked or cracked token is a permanent key. A token is only proof of identity if it can't be minted or reused by someone else.

Admin routes are guarded only in the UI. The AI hides the admin nav link from non-admins and calls it done, but the actual API routes have no server-side role check. Anyone who knows the URL runs admin functions. Hiding a button is not access control.

Group 2: trusting what they send

The caller is who they say — but the app believes the values they hand it.

IDOR — the object id in the URL. /api/invoices/1234 returns invoice 1234 without checking the logged-in user owns it. Change it to 1235, 1236, 1237 and read everyone's data. This is OWASP's top risk and the single most common logic bug in AI-built apps: the model writes "get the invoice by id" and never adds "and confirm it belongs to this user." Sequential ids make it trivial; scanners miss it because the endpoint works.

The price comes from the client. Checkout sends amount or price in the request body and the server charges it. Open DevTools, set it to 1, pay one cent. Prices must be looked up server-side from your own catalog, never accepted from the browser.

The download link trusts "you have the URL, so you're allowed." No login, no expiry, an id that's a timestamp plus a few random characters, sitting in an inbox forever. Guess one or find a forwarded email and you pull down someone's private files. Links to anything confidential should be signed and short-lived.

User input trusts itself all the way into the output. A project name, a note, a URL goes straight into the HTML of an email or a dashboard. Name something <img src=x onerror=...> and it runs inside the reader's browser. Every value a user controls has to be escaped where it lands, and only real http(s) links allowed to become links.

Group 3: doors left labeled

Not "trusting the caller" exactly — leaving the map to the vault in plain sight.

Secrets in the frontend bundle. Stripe secret keys, OpenAI or Anthropic keys, database URLs shipped in client-side JavaScript because the AI didn't distinguish "runs in the browser" from "runs on the server." Anyone opens DevTools and reads them. This is one of the most common findings in the wild, and the fix is architectural, not a patch: secrets never touch code that ships to the browser.

Verbose errors and debug endpoints. A stack trace that names your framework, your database, your file paths. A /health or /debug route that cheerfully reports which secrets are configured. Each one is a free reconnaissance map. Errors shown to users should say what went wrong, not how the system is built.

Nothing is rate limited. Every hole above gets multiplied by ten thousand attempts because no endpoint counts how often a caller hits it. Rate limiting is what turns "possible" into "not worth it."

CORS set to allow everyone. A wildcard origin so any website can call your API with a logged-in user's credentials. The AI set it wide to make the error go away during development and never narrowed it.

And the one that hides in the database

There's a deeper version of all this at the database layer — where row-level isolation is switched on but pointed at nothing, and one account reads another's rows even though every setting looks green. It's common enough and subtle enough that it gets its own writeup: the cross-tenant leak an agent shipped me. If your app is multi-tenant, read that one too.

Why a scanner shrugs at most of this

A scanner reads configuration. It sees a webhook handler and a signature header, an endpoint that returns data, a JWT library in the dependencies, and it moves on. It can't tell the signature is never verified, that the endpoint never checks ownership, that the token never expires — because "verify," "check ownership," "expire" are intent, not syntax. The handler exists. The call succeeds. It looks complete. What's missing is the question the tool doesn't know to ask: is this caller who they claim, and are they allowed to do this? Config-level findings a scanner catches. The access-control logic that causes the real harm, only a human reading the code with that question in mind will find.

The half AI won't write unless you ask

Building the honest path is the half AI does well. The adversary's half is small, mechanical, and almost always missing. A few of the fixes, in the shape they take:

webhook:   no valid signature   →  400, ignored        (not 200 + email)
invoice:   not your object      →  403                  (not 200 + data)
checkout:  price from server    →  charge the catalog   (not the request body)
download:  no valid signature   →  403                  (not the file)

None of it is hard. What's missing isn't skill — it's the instruction. "Make it work" gets said out loud. "Make sure it can't be abused" doesn't, so the model never writes it.

Why "it works" isn't the finish line

A demo only ever walks the happy path. It pays with a real card, uploads a real zip, receives a real email. It never forges the webhook, never increments the invoice id, never sets the price to a penny, never submits without paying. So the demo passes and every hole above stays quiet — until someone who isn't being honest goes looking, and on a site that moves money, someone always does.

If an agent built your payment flow fast, the golden path passing tells you almost nothing. The question is what happens on the paths you never demoed. Run this guide against your own app, category by category — or if you'd rather someone did it adversarially, with the one question that finds these, that part I can help with.

Worried your AI-built app has one of these?

I review AI-generated apps for exactly these holes — and fix them before launch.

See how it works →