Ask an AI coding assistant to build a feature, and it will. Ask it to build a secure feature, and most of the time it still just builds the feature — it demos perfectly, it ships fast, and the security thinking that used to happen somewhere in a code review never happens at all.
We call this vibe coding: describing what you want in plain language and accepting whatever comes back because it runs. In a recent assessment, we reviewed an application that was, by every indication, vibe-coded end to end — and the shortcuts were strikingly consistent. Here's what we keep finding.
The missing question: "whose data is this?"
This is the single most common thing we find. Somewhere in the codebase there's an endpoint like "get document by ID," and it does exactly what it says: look the record up by its primary key, return it. Nobody asked whether the caller actually owns that record, so the code never checks.
The giveaway is usually sitting right next to the bug. The same application will often have a "list my own documents" endpoint that correctly filters by the caller's account, because the prompt that produced it was naturally phrased around "my" or "the current user." The sibling endpoint that fetches a single record by ID was phrased more generically, so the model wrote the generic, unscoped version.
The result in practice: any authenticated user, including one who just self-registered a free account, can enumerate small integer IDs and read or download every other user's data. It's not subtle once you're looking for it. Nothing in a vibe-coding loop ever prompts anyone to look.
Secrets that ship with the build
Token-signing keys, encryption keys, third-party plugin tokens — sitting in plaintext in a config file, right next to the rest of the application's settings. Not leftover test values: often human-typed, keyboard-mash-looking strings, written in the moment because a secret had to go somewhere for the feature to run.
An assistant asked to "add authentication" will produce working authentication. It will not, on its own initiative, provision a secrets manager or flag that the key it just wrote needs to be rotated before anyone relies on it — that only happens as a deliberate, separate step, and vibe-coded builds skip the slow stuff. We've even seen the same key reused to both sign tokens and encrypt unrelated stored credentials, because copying one value was faster than provisioning two.
Whatever was already in the container
Vibe-coded applications tend to bundle whatever third-party component got them to a working demo fastest, then never look at it again.
We've found production deployments shipping a native media-processing binary that turned out to be an untagged, unreleased development build — not a numbered stable version at all — carried forward because it worked the first time someone tried it. In that window, a critical, publicly documented remote-code-execution bug in that exact component had been disclosed and fixed upstream. The deployment was still on the vulnerable side of that fix.
The same pattern shows up at the dependency level: packages pinned to whatever version the scaffolding happened to install. An assistant has no built-in instinct to check a CVE database before writing a package reference — it reaches for whatever's idiomatic, and idiomatic isn't the same as current.
No one modeled the attacker
Login forms get built to do one thing: let a legitimate user log in. They're rarely built to also ask what happens when someone isn't legitimate.
Most modern frameworks lock an account out after a handful of failed attempts by default — a feature you'd have to deliberately turn off to lose. We've seen it turned off on every login surface in an application, almost certainly because a failed test run kept getting locked out mid-development and the fix was to disable the protection rather than reset the test account. Neither lockout nor rate limiting is hard to build — both are usually one configuration call away in the framework already being used. They just aren't part of "make login work."
Why this keeps happening
All four patterns share one root cause. An AI coding assistant is optimized to satisfy the prompt as given and to produce something that works when used the way it's supposed to be used. Adversarial thinking — how would someone abuse this instead of use it — isn't part of that loop unless a human deliberately brings it in.
A junior developer eventually absorbs "always check ownership" as an instinct, through code review, through a senior engineer asking "what stops me from just changing this ID in the URL," through getting it wrong once and remembering. A model doesn't carry that instinct forward from one generation to the next. It carries forward only what's in the context window in front of it — and vibe coding, by design, skips the slow review step where that instinct would normally get applied.
What to do about it before you ship
None of this is an argument against using AI to write code. It's an argument for putting back the step vibe coding is designed to skip.
- Review AI-generated code the way you'd review a junior engineer's pull request, specifically for authorization: does every endpoint that takes an ID check who's asking, not just what they're asking for?
- Run dependency and component scanning as a standing part of the build pipeline, not a one-time check before launch. "It's a popular library" says nothing about whether the pinned version is patched.
- Ask your assistant the adversarial question directly: "what happens if an authenticated user who doesn't own this record requests it anyway?" Models are often capable of catching their own gap — they just won't volunteer the question unprompted.
- Get a second, human, adversarial set of eyes on anything handling real user data before it reaches production. A model reviewing its own code has the same blind spot a developer has reviewing their own pull request.
AI tooling has made it faster than ever to build something that works. It hasn't changed what it takes to build something that holds up against someone trying to break it.
Get a quote