Five security patterns that appear in AI-generated code — and why code review usually misses them
The specific vulnerabilities AI coding assistants introduce, and five questions to add to your review process.
A 2026 analysis of AI-generated codebases found they contain 2.5 times more CVSS 7.0+ vulnerabilities than equivalent human-written code. That figure gets passed around as a reason to distrust AI coding tools. It's a misread of the data.
The more useful finding is that the vulnerabilities cluster. They are not randomly distributed across the codebase. They appear in five recognisable patterns, and all five emerge from the same root cause: AI coding assistants complete code from the most plausible examples they've seen, not from an understanding of your specific threat model.
AI-generated code security is a tractable problem once you know the patterns. A team that updates its code review for these five specific failure modes will catch most of what matters. The investment is five targeted questions added to every review, no new tool required.
What the 2026 data actually shows
Sherlock Forensics' 2026 report put the headline figure at 92% of AI-generated codebases containing at least one critical vulnerability. Kusari found a 4× productivity gain alongside a 10× increase in vulnerability risk per code-change. The fine print in both reports: the vulnerabilities are not evenly spread.
Design-level security flaws increased 153% in AI-generated code, with authentication bypass and improper session management leading the count. Secrets exposure rose 40%. Cross-site scripting appeared at 2.74 times the rate of human-written code. These are not new vulnerability categories. They're familiar patterns appearing in unusual places because the code was generated by a system optimising for correct-looking output, not secure output.
Understanding why they appear is more useful than knowing they appear. Each pattern below has a specific mechanical explanation, and each one has a specific fix.
Pattern 1: Credentials that travel further than they should
AI assistants handle the obvious case. When you ask for an API call, you get process.env.API_KEY. What they miss is the secondary credential journey.
The specific failure mode: credentials end up in log statements, not in source files. When you ask an AI assistant to "add connection logging to this database pool" or "debug why this service is failing", it will often produce code like this:
const pool = new Pool({ connectionString: config.db.url });
logger.debug('Database pool created', { poolConfig: config.db });
// config.db contains the URL — which contains the passwordThe config.db object includes the connection URL, which includes the password. Structured logging shipped to a centralised aggregator has now received your database password. A grep for credentials in source files will miss this entirely. The password is in a data structure, not a string literal.
A second variant: credentials appear in error objects that get serialised to JSON and returned in API responses during debugging, then not stripped before the code ships. The AI included them because they're useful for debugging; it doesn't know you're about to deploy this.
Pattern 2: Auth scaffolding that graduates to production
When you're building fast with an AI assistant, the first draft of an endpoint often skips auth. You add a note. You ask the AI to complete the feature. What you're doing is asking the AI to build a real system around a development placeholder — and it will.
The specific failure: if (process.env.SKIP_AUTH === 'true') checks, TODO: add auth comments above unconditional handlers, bypass routes added for testing under /internal/ or /debug/ paths. Each of these was temporary during development. The AI completed the feature around them and preserved them, because the pattern existed in your codebase and there was nothing to signal it was temporary.
app.get('/api/users/:id', async (req, res) => {
// Added for local dev — REMOVE BEFORE SHIP
if (process.env.SKIP_AUTH === 'true') {
return getUserHandler(req, res);
}
await verifyToken(req);
return getUserHandler(req, res);
});
// The comment didn't make it into production. The bypass did.AI assistants are particularly poor at distinguishing "this is scaffolding" from "this is intent". If your codebase has environment-variable auth bypasses, generated code will carry them forward.
Pattern 3: Input validation at one layer only
AI assistants write validation where tutorials put it: at the handler layer. A well-generated Express or FastAPI endpoint validates the incoming body, checks required fields, rejects wrong types. The service method it calls does not.
This creates a vulnerability when:
- A background job calls the service method directly, passing data from a queue without handler-level validation
- A webhook handler receives external data and calls the service with it
- An internal gRPC or inter-service call reaches the service without going through the HTTP handler
- Tests call the service directly and don't replicate the validation
The AI doesn't know the codebase will grow these additional callers. It generates a validated handler and an unvalidated service, which was fine for the tutorial it learned from — where the handler was the only caller.
Pattern 4: Injection through helper abstractions
Raw SQL string concatenation is something AI assistants have largely learned to avoid. The subtler failures are in the ORM and query builder layer — the places where the library is used correctly for the happy path, but the API choice creates an injection vector for edge cases.
Three examples that appear in AI-generated code:
// Pattern: using .raw() to handle a dynamic column
// The AI chose .raw() because it's the most flexible option
const results = await knex.raw(
'SELECT * FROM documents WHERE owner = ' + userId
);
// Pattern: MongoDB $where with string input
// The AI chose $where for maximum query flexibility
db.collection('users').find({
$where: "this.role === '" + userInput + "'"
});
// Pattern: Redis key built from unsanitised input
// Seems harmless; in practice enables key injection
const cacheKey = namespace + ':' + req.params.userId;
await redis.get(cacheKey);The common thread: the AI chose the most permissive API because it handles all cases. The vulnerability is in the API choice, not in obvious string concatenation. These won't trigger a simple injection scanner.
Pattern 5: Insecure defaults from development-focused examples
AI assistants generate code that works in development. Development environments have loose defaults. When those defaults travel to production without review, they become vulnerabilities.
The specific failures that appear most often in AI-generated code:
| Pattern | What the AI generated | Production risk |
|---|---|---|
| CORS wildcard | cors({ origin: '*' }) | Allows any origin to make authenticated cross-origin requests |
| JSON body size | express.json() | No size limit — allows arbitrarily large request bodies |
| JWT without audience | jwt.verify(token, secret) | Accepts tokens issued for other services in the same key space |
| Session cookie flags | session({ secret }) | Missing SameSite: strict and secure: true by default in many libraries |
| Shell execution | child_process.exec(cmd) | exec runs in a shell; execFile is safer for fixed commands |
These aren't exotic vulnerabilities. They're the choices an AI makes when the goal is "make it work" rather than "make it work securely in this threat model". The AI learned from examples where the code had to run, not examples where the code had to be production-safe.
Five questions to add to your code review
Standard code review asks whether code works, whether it's readable, and whether it has tests. That process doesn't surface the patterns above. Adding five targeted questions catches most of them.
The questions:
- Credential exposure: Where does this credential come from, and does any log statement receive a containing object?
- Auth completeness: Is there any path to call the service layer without hitting the auth check — including background jobs and webhook handlers?
- Validation coverage: What validates this input at the service layer, not just at the handler? What happens when the service is called directly?
- Query injection: Does any query builder or cache key receive a value from user input, even indirectly through a helper?
- Default review: What library defaults are active here? Were they set up for a development or production environment?
These are review questions, not static analysis rules. A scanner will catch credentials in source files and obvious string concatenation. It will not catch "was this auth bypass meant to be permanent" or "does this library default match the deployment context". The questions require a reviewer to check intent, not just implementation.
“The five patterns are predictable because they emerge from how LLMs generate plausible completions — which means they're catchable.”
The underlying issue
All five patterns share a cause. AI coding assistants complete code from the most plausible next tokens given their training data — not from an analysis of your threat model, your deployment context, or whether the code they're completing is scaffolding or intent. They are very good at making code that works. They are not good at making code that is secure by default in an environment they don't know.
That's not a criticism — it's a description. The productivity gains from AI coding tools are real and consistent across the 2026 research. The security risk is also real and manageable. The adjustment is specific: five questions in code review, applied to every block of AI-generated code that touches auth, input handling, logging, database access, or runtime configuration.
Three minutes of targeted questions per review is a small cost against the velocity gain. The teams that get this right don't audit AI-generated code differently from human-written code — they ask the same five questions of both.
Frequently asked questions
Related reading
Feature flags in production: the lifecycle teams skip
Most teams have a system for adding feature flags. Almost none have a system for retiring them. Here is the full lifecycle: flag types, staleness detection, and the cleanup playbook.
The minimum viable security posture for a 10-person SaaS
Most security advice targets enterprises or absolute beginners. Eight controls for a 10-person B2B SaaS team — ranked by how much breach risk each closes per hour of engineering work.
AI agents in production: the cost controls most teams build too late
Most teams discover that their AI agent has been burning money the wrong way after the invoice arrives. Five operational controls prevent that — and most teams build them too late.