OWASP Top 10 in AI-Generated Code: Which Vulnerabilities Appear Most Often
July 8, 2026 · 9-minute read · Fairy
The short answer
Yes, AI-generated code frequently contains OWASP Top 10 vulnerabilities. The most common are Broken Access Control (missing auth checks, no RLS), Identification and Authentication Failures (JWTs without expiration, weak password storage), Security Misconfiguration (unvalidated webhooks), and Injection flaws. These patterns appear because LLMs optimize for functionality over security, often omitting defensive code that doesn't affect happy-path behavior.
Yes, AI-Generated Code Contains OWASP Top 10 Vulnerabilities
AI-generated code frequently ships with OWASP Top 10 vulnerabilities. Based on patterns observed across production code reviews, the most common categories are Broken Access Control (A01), Identification and Authentication Failures (A07), Security Misconfiguration (A05), and Injection (A03). These vulnerabilities appear because large language models optimize for code that works, not code that's secure—and many security measures don't affect whether the happy path executes successfully.
This isn't theoretical. When AI generates a Stripe webhook handler, it often skips signature verification. When it creates a protected API route, it frequently omits the authentication middleware. When it stores passwords, it sometimes uses SHA-256 instead of bcrypt. The code runs. The tests pass. The vulnerability ships.
Below is a detailed mapping of OWASP Top 10 categories to the patterns that appear most often in AI-generated code, with concrete examples of what the vulnerable code looks like versus the secure alternative.
A01: Broken Access Control — The Most Common Category
Broken Access Control tops the OWASP list, and it dominates AI-generated code reviews. LLMs consistently produce code that grants access without verifying the user should have it.
Pattern: Protected Routes Missing Auth Checks
AI-generated APIs frequently expose endpoints that should require authentication but don't. The auth middleware exists elsewhere in the codebase, but the new route doesn't use it.
Vulnerable pattern (AI-generated):
// AI generates the route, forgets the middleware
app.get('/api/user/settings', async (req, res) => {
const settings = await db.settings.findAll();
res.json(settings);
});
Secure pattern:
// Auth middleware must wrap protected routes
app.get('/api/user/settings', requireAuth, async (req, res) => {
const settings = await db.settings.findByUser(req.user.id);
res.json(settings);
});
The vulnerable version has two problems: no authentication check and no scoping to the current user. Both are invisible to an LLM optimizing for "code that returns settings."
Pattern: Database Tables Without Row-Level Security
When AI generates Supabase or PostgreSQL schemas, it often creates tables without enabling Row-Level Security (RLS). The table works—every query succeeds—but every row is readable and writable by any authenticated user.
Vulnerable pattern (AI-generated):
CREATE TABLE user_documents (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID REFERENCES auth.users(id),
content TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
-- No RLS enabled, no policies defined
Secure pattern:
CREATE TABLE user_documents (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID REFERENCES auth.users(id),
content TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
ALTER TABLE user_documents ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can only access their own documents"
ON user_documents
FOR ALL
USING (auth.uid() = user_id);
This pattern appears in nearly every AI-generated Supabase project we review. The fix is straightforward, but LLMs don't add it unprompted because the table works without it.
A07: Identification and Authentication Failures
Authentication code is where AI causes the most damage per line. The patterns are subtle, the failures are silent, and the consequences are catastrophic.
Pattern: JWTs Signed Without Expiration
AI generates JWT signing code that omits the expiresIn option. The token works indefinitely. If it's ever leaked—in logs, in a browser, through XSS—it's valid forever.
Vulnerable pattern (AI-generated):
const token = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET
);
Secure pattern:
const token = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
One option. Three characters. The difference between a token that expires and one that's a permanent backdoor.
Pattern: OAuth State Parameter Not Validated
OAuth flows require a state parameter to prevent login CSRF attacks. AI-generated OAuth code often generates the authorization URL correctly but skips validation in the callback.
Vulnerable pattern (AI-generated):
// Callback handler - no state validation
app.get('/auth/callback', async (req, res) => {
const { code } = req.query;
const tokens = await oauth.getTokens(code);
// Proceeds without checking state
});
Secure pattern:
// Generate state on auth start
app.get('/auth/login', (req, res) => {
const state = crypto.randomBytes(32).toString('hex');
req.session.oauthState = state;
res.redirect(oauth.getAuthUrl({ state }));
});
// Validate state on callback
app.get('/auth/callback', async (req, res) => {
const { code, state } = req.query;
if (state !== req.session.oauthState) {
return res.status(403).send('Invalid state');
}
delete req.session.oauthState;
const tokens = await oauth.getTokens(code);
});
The attack: an attacker initiates OAuth with their account, intercepts the callback URL, and tricks a victim into clicking it. The victim's session is now logged into the attacker's account. Without state validation, the application can't distinguish legitimate callbacks from forged ones.
Pattern: Passwords Stored Without a Key Derivation Function
AI occasionally generates password storage using fast hashes (MD5, SHA-256) or even plaintext comparison. These are crackable at billions of attempts per second on commodity hardware.
Vulnerable pattern (AI-generated):
// SHA-256 is not a password hash
const hashedPassword = crypto
.createHash('sha256')
.update(password)
.digest('hex');
await db.users.insert({ email, password: hashedPassword });
Secure pattern:
// bcrypt is slow by design
const hashedPassword = await bcrypt.hash(password, 12);
await db.users.insert({ email, password: hashedPassword });
// Verification with timing-safe comparison
const valid = await bcrypt.compare(inputPassword, user.password);
The cost factor (12) makes each hash attempt take ~250ms. An attacker cracking SHA-256 can try billions per second; with bcrypt, they get a few per second.
A05: Security Misconfiguration
Security misconfigurations in AI code usually take the form of missing validation on external inputs—particularly webhooks.
Pattern: Stripe Webhooks Without Signature Verification
AI generates Stripe webhook handlers that process events without verifying they actually came from Stripe. Anyone who knows the endpoint URL can forge events.
Vulnerable pattern (AI-generated):
app.post('/webhooks/stripe', async (req, res) => {
const event = req.body; // Parsed JSON, not verified
if (event.type === 'payment_intent.succeeded') {
await fulfillOrder(event.data.object);
}
res.sendStatus(200);
});
Secure pattern:
app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), async (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body, // Raw body, not parsed
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
return res.status(400).send(`Webhook signature verification failed`);
}
if (event.type === 'payment_intent.succeeded') {
await fulfillOrder(event.data.object);
}
res.sendStatus(200);
});
Critical detail: signature verification requires the raw request body. If the body is parsed as JSON before verification, the signature won't match. AI frequently gets this wrong, using req.body after express.json() middleware has already parsed it.
Pattern: Webhook Handlers Without Idempotency
Even with signature verification, AI-generated webhook handlers typically lack idempotency checks. Stripe retries webhooks on timeout or 5xx responses. Without deduplication, the same event processes multiple times.
Vulnerable pattern (AI-generated):
// Processes every event, including retries
if (event.type === 'payment_intent.succeeded') {
await fulfillOrder(event.data.object);
}
Secure pattern:
if (event.type === 'payment_intent.succeeded') {
// Skip if already processed
if (await redis.get(`webhook:${event.id}`)) {
return res.sendStatus(200);
}
await redis.set(`webhook:${event.id}`, '1', 'EX', 86400);
await fulfillOrder(event.data.object);
}
Duplicate fulfillment is a real production issue. Orders ship twice. Subscriptions grant double credits. Refunds process multiple times.
A08: Software and Data Integrity Failures
This category includes the webhook signature issue above, but also covers a subtler pattern: access granted before payment is actually confirmed.
Pattern: Fulfillment on Payment Intent Creation
AI-generated payment flows sometimes grant access when the PaymentIntent is created rather than when payment succeeds. The intent represents an attempt to pay, not a successful payment.
Vulnerable pattern (AI-generated):
app.post('/api/purchase', async (req, res) => {
const paymentIntent = await stripe.paymentIntents.create({
amount: 1000,
currency: 'usd',
});
// WRONG: Access granted before payment succeeds
await grantAccess(req.user.id, req.body.productId);
res.json({ clientSecret: paymentIntent.client_secret });
});
Secure pattern:
// Grant access only in webhook after payment succeeds
app.post('/webhooks/stripe', async (req, res) => {
const event = verifyWebhook(req);
if (event.type === 'payment_intent.succeeded') {
const { metadata } = event.data.object;
await grantAccess(metadata.userId, metadata.productId);
}
});
The user gets the product before their card is charged. If the payment fails, they keep access.
A03: Injection
Classic SQL injection is less common in AI-generated code because ORMs handle parameterization. But injection still appears in dynamic queries, raw SQL, and NoSQL contexts.
Pattern: String Interpolation in Database Queries
When AI generates complex queries that don't fit ORM patterns, it sometimes falls back to string interpolation.
Vulnerable pattern (AI-generated):
// Dynamic column or table names are danger zones
const results = await db.query(
`SELECT * FROM ${tableName} WHERE status = '${status}'`
);
Secure pattern:
// Whitelist allowed values, parameterize user input
const allowedTables = ['orders', 'users', 'products'];
if (!allowedTables.includes(tableName)) {
throw new Error('Invalid table');
}
const results = await db.query(
`SELECT * FROM ${tableName} WHERE status = $1`,
[status]
);
The underlying issue: LLMs learn from code that works. String interpolation works. The vulnerability is invisible in normal operation.
Why LLMs Produce These Patterns
Understanding the root cause helps predict where to look:
-
Security code doesn't affect happy-path behavior. An auth check that's missing doesn't break the feature—it just makes it insecure. LLMs optimize for "code that works."
-
Security patterns are verbose. Signature verification adds 10 lines. Idempotency adds 5. JWTs need options objects. LLMs generate minimal code by default.
-
Training data contains both secure and insecure examples. Stack Overflow answers, blog posts, and tutorials often omit security for brevity. LLMs reproduce what they've seen.
-
Context windows lose security requirements. The security requirement might be in a README or a comment 500 lines away. The LLM generating the webhook handler doesn't see it.
What This Means for Teams Using AI Code Generation
AI-generated code requires security review before deployment. The patterns above appear in production code from every major LLM—not because the models are broken, but because security is an emergent property of systems, not individual functions.
Teams deploying AI-generated code need:
- Verification before merge: Expert review of authentication, authorization, and payment flows. These are where AI makes critical errors.
- Continuous monitoring: Watch for drift as models update and prompts change. A pattern that was secure last month might not be today.
- Institutional knowledge: The context about why a particular check exists needs to persist across sessions. LLMs don't remember previous conversations.
This is the infrastructure gap that Fairy for Code addresses. AI does the work—generating the initial implementation, writing tests, building features. Fairy makes it reliable by ensuring experts verify the patterns that LLMs consistently get wrong.
For a quick check on your current codebase, Fairy Scout provides free automated review that catches many of these patterns before they reach production.
Conclusion
AI-generated code contains OWASP Top 10 vulnerabilities at predictable rates in predictable places. Broken Access Control and Authentication Failures are the most common, followed by Security Misconfiguration and Injection. The patterns are consistent across LLMs because they stem from how these models optimize—for functionality, not security.
The solution isn't to stop using AI for code generation. It's to build verification into your deployment process. Know where AI fails, review those areas systematically, and ensure the humans responsible for security actually see the code before it ships.
Frequently asked questions
Which OWASP category is most common in AI-generated code?
Broken Access Control (A01) is the most prevalent. AI frequently generates routes without authentication checks, database queries without row-level security, and APIs that skip authorization verification. These omissions don't break functionality, so LLMs don't catch them.
Does GitHub Copilot generate code with security vulnerabilities?
Yes. Research and production reviews show Copilot and similar LLMs produce code with authentication gaps, missing input validation, and insecure defaults. The rate varies by context, but security-critical code requires human verification regardless of the generation tool.
How can teams prevent OWASP vulnerabilities in AI-generated code?
Implement mandatory security review before merge, use static analysis tools configured for OWASP patterns, and establish verification workflows where experts check authentication, authorization, and input handling. Never deploy AI-generated security-sensitive code without review.
Are AI code assistants safe for authentication code?
AI assistants frequently produce authentication code with critical flaws: JWTs without expiration, passwords stored with weak hashing, and OAuth flows missing state validation. Authentication code from any AI tool should be reviewed by someone who understands the security requirements.
Have AI-generated work you’d want verified? Connect with a Fairy → or run a free check with Scout.
More resources