OWASP Top 10:2025 Cheat Sheet for AI-Generated Code
Every OWASP Top 10:2025 risk category with real examples of how Copilot, Cursor, and Claude reproduce them — and what to look for before code ships.

TL;DR
AI models reproduce all 10 OWASP Top 10:2025 vulnerability classes. The most common in generated code: A03 (SQL injection via string concatenation), A02 (hardcoded secrets), and A07 (jwt.decode() instead of jwt.verify()). CodeSlick catches A01–A03 and A07–A08 automatically on every PR. A04 (insecure design) and A09 (missing logging) still require human review.
The OWASP Top 10:2025 landed with updated statistics, and the categories that rose are the ones AI coding tools fail at most often.
Scope note: This covers the OWASP Web Application Top 10:2025 — the established list of web security risks. It is not the OWASP LLM Top 10, which covers risks specific to AI systems themselves (Prompt Injection, Excessive Agency, Training Data Poisoning, and others). That list matters — but this post is about the web vulnerabilities AI tools generate in your codebase.
This is not a criticism of AI tools. It is a structural problem. Models are trained on code that predates modern security practices, written under deadline pressure, and rarely reviewed for security. A model that generates code that compiles and passes your test suite has done its job. Whether it hardcoded a secret or skipped input validation is not part of the brief.
These bugs predate AI by decades. What AI changes is the rate at which they ship. A developer on a deadline might write one SQL injection; a model on autopilot writes it in every handler that touches user input, with the same confident style as the correct code around it. The risk is not that the model is uniquely bad — it is that it removes the friction that used to slow down known-bad patterns.
This cheat sheet covers each OWASP 2025 category: what AI-generated versions look like, a code example, and what to flag. Jump to any category using the links below.
A01:2025 — Broken Access Control
Access control failures occur when users can act outside their intended permissions. AI models implement the happy path (authenticated user gets data) without the ownership check that prevents user A from reading user B's records.
How AI generates it:
// AI-generated: fetches by ID with no ownership check
app.get('/api/orders/:id', authenticate, async (req, res) => {
const order = await db.query(
'SELECT * FROM orders WHERE id = $1',
[req.params.id]
);
res.json(order.rows[0]);
});// Ownership check added
app.get('/api/orders/:id', authenticate, async (req, res) => {
const order = await db.query(
'SELECT * FROM orders WHERE id = $1 AND user_id = $2',
[req.params.id, req.user.id]
);
if (!order.rows[0]) return res.status(403).json({ error: 'Forbidden' });
res.json(order.rows[0]);
});What to look for: route handlers that query by ID from params/path without a corresponding user ownership condition.
A02:2025 — Cryptographic Failures
Cryptographic failures cover hardcoded secrets, weak algorithms, and disabled TLS verification. Secrets get hardcoded because the model's training corpus is full of code where credentials were never moved to environment variables. InsecureSkipVerify: true appears because models have seen it used as a dev workaround and reproduce it without context.
How AI generates it:
# AI-generated: hardcoded AWS credentials
import boto3
AWS_ACCESS_KEY = 'AKIAIOSFODNN7EXAMPLE'
AWS_SECRET_KEY = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
client = boto3.client(
's3',
aws_access_key_id=AWS_ACCESS_KEY,
aws_secret_access_key=AWS_SECRET_KEY,
)import boto3
import os
client = boto3.client(
's3',
aws_access_key_id=os.environ['AWS_ACCESS_KEY_ID'],
aws_secret_access_key=os.environ['AWS_SECRET_ACCESS_KEY'],
)What to look for: string literals matching secret patterns (AWS key format, JWT secrets, API key prefixes). InsecureSkipVerify: true in Go TLS config. MD5/SHA1 used for password hashing.
A03:2025 — Injection
SQL injection via string concatenation is the single most common vulnerability in AI-generated code. The training data contains thousands of query-building examples using string formatting. It works. It is readable. It is wrong. The same pattern appears in shell commands, LDAP queries, and template engines.
How AI generates it:
// AI-generated: string concatenation in SQL
async function getUser(username) {
const result = await db.query(
"SELECT * FROM users WHERE username = '" + username + "'"
);
return result.rows[0];
}
// Also common: eval() on user input
function applyFilter(data, filterExpr) {
return data.filter(item => eval(filterExpr));
}// Parameterized query
async function getUser(username) {
const result = await db.query(
'SELECT * FROM users WHERE username = $1',
[username]
);
return result.rows[0];
}What to look for: query strings with + concatenation or template literals containing variables. eval(), Function(), exec() with user-controlled input. Python % formatting or .format() in SQL strings.
A04:2025 — Insecure Design
Insecure design is a structural flaw, not a bad line of code. Ask a model for a password reset flow and it generates one that works — no rate limiting, no token expiry, no account enumeration protection. The model has no incentive to add constraints you did not ask for.
How AI generates it:
// AI-generated: password reset with no rate limit or expiry
app.post('/api/reset-password', async (req, res) => {
const { email, token, newPassword } = req.body;
const user = await db.findOne({ email, resetToken: token });
if (!user) return res.status(400).json({ error: 'Invalid token' });
await db.updatePassword(user.id, newPassword);
res.json({ success: true });
});
// Missing: token expiry check, rate limiting, token invalidation after useWhat to look for: sensitive flows (reset, MFA, payment) with no rate limiting middleware. Token validation without expiry checks. Account existence revealed through different response times or error messages.
A05:2025 — Security Misconfiguration
Permissive CORS, debug endpoints, and default credentials appear constantly in tutorials and Stack Overflow answers. That is what the model was trained on, and that is what it generates.
How AI generates it:
// AI-generated: wildcard CORS + debug mode
const app = express();
app.use(cors({ origin: '*', credentials: true }));
app.use(express.json());
// Debug endpoint left in
app.get('/debug/config', (req, res) => {
res.json(process.env); // exposes all environment variables
});const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [];
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) callback(null, true);
else callback(new Error('Not allowed by CORS'));
},
credentials: true,
}));What to look for: cors({ origin: '*' }), debug routes that expose config or stack traces, default admin credentials in seed files.
A06:2025 — Vulnerable and Outdated Components
Models pin to versions that were current when their training data was collected, typically 12–18 months behind the registry. lodash@4.17.15 carries CVE-2021-23337 (prototype pollution, fixed in 4.17.21). jsonwebtoken@8.5.1 carries CVE-2022-23529 (algorithm confusion, fixed in 9.0.0). Both remain the canonical examples of AI-introduced stale dependencies as of 2026.
How AI generates it:
{
"dependencies": {
"lodash": "4.17.15",
"jsonwebtoken": "8.5.1",
"axios": "0.21.1",
"express": "4.17.1"
}
}What to look for: pinned package versions with known CVEs. Dependencies untouched for 12+ months. Transitive vulnerabilities in lockfiles (check OSV.dev or the GitHub Advisory Database). npm audit, Dependabot, and Snyk all catch this category too — A06 is one place where multiple overlapping tools give you better coverage than any single one.
A07:2025 — Identification and Authentication Failures
The most common AI-generated authentication failure: jwt.decode() instead of jwt.verify(). Decode reads the payload without checking the signature, so a forged token passes. Both functions appear in documentation and the difference is not obvious from the name alone.
How AI generates it:
import jwt from 'jsonwebtoken';
// jwt.decode() does NOT verify the signature
export function getUser(token: string) {
const payload = jwt.decode(token); // anyone can forge this
return payload as { userId: string; role: string };
}import jwt from 'jsonwebtoken';
export function getUser(token: string) {
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!);
return payload as { userId: string; role: string };
} catch {
return null;
}
}What to look for: jwt.decode() in authentication middleware. Missing password complexity enforcement. Sessions that never expire. Passwords stored with MD5 or SHA1 instead of bcrypt/argon2.
A08:2025 — Software and Data Integrity Failures
pickle.loads() on untrusted data executes arbitrary code — not a theoretical risk, an explicit Python feature. Models reach for pickle because it is the simplest serialization option. The execution behavior does not appear in the examples they learned from.
How AI generates it:
import pickle
import base64
def restore_session(session_cookie: str):
# Arbitrary code execution if cookie is tampered
data = base64.b64decode(session_cookie)
return pickle.loads(data) # CRITICAL: never deserialize untrusted dataimport json
import base64
def restore_session(session_cookie: str):
data = base64.b64decode(session_cookie)
return json.loads(data) # JSON only deserializes data, not codeWhat to look for: pickle.loads(), marshal.loads(), or yaml.load() (without Loader=yaml.SafeLoader) on any data from requests, cookies, or external systems. Unsigned data passed to eval or exec.
A09:2025 — Security Logging and Monitoring Failures
Models generate minimal logging because training examples focus on the success path. Failed logins, access rejections, and validation errors are absent from most code samples, so they are absent from generated code. Attacks go undetected until they succeed.
How AI generates it:
// AI-generated: silent auth failures
app.post('/api/login', async (req, res) => {
const user = await db.findByEmail(req.body.email);
if (!user || !bcrypt.compareSync(req.body.password, user.hash)) {
return res.status(401).json({ error: 'Invalid credentials' });
// No log: attacker can brute-force silently
}
res.json({ token: generateToken(user) });
});app.post('/api/login', async (req, res) => {
const user = await db.findByEmail(req.body.email);
if (!user || !bcrypt.compareSync(req.body.password, user.hash)) {
logger.warn('Failed login attempt', {
email: req.body.email,
ip: req.ip,
timestamp: new Date().toISOString(),
});
return res.status(401).json({ error: 'Invalid credentials' });
}
logger.info('Successful login', { userId: user.id, ip: req.ip });
res.json({ token: generateToken(user) });
});What to look for: authentication handlers with no logging on failure paths. Admin actions with no audit trail. Error handlers that swallow exceptions silently.
A10:2025 — Server-Side Request Forgery
SSRF: a server fetches a URL that the user controls. In webhook handlers, URL preview features, and any external-content fetch, the obvious implementation is to take the URL from the request and call it directly. The model does exactly that.
How AI generates it:
// AI-generated: fetches any URL the user provides
app.post('/api/preview', async (req, res) => {
const { url } = req.body;
const response = await fetch(url); // attacker can target internal services
const html = await response.text();
res.json({ preview: html.slice(0, 500) });
});const { URL } = require('url');
const ALLOWED_PROTOCOLS = ['http:', 'https:'];
const BLOCKED_HOSTS = ['localhost', '127.0.0.1', '0.0.0.0', '169.254.169.254'];
app.post('/api/preview', async (req, res) => {
let parsed;
try { parsed = new URL(req.body.url); } catch {
return res.status(400).json({ error: 'Invalid URL' });
}
if (!ALLOWED_PROTOCOLS.includes(parsed.protocol))
return res.status(400).json({ error: 'Protocol not allowed' });
if (BLOCKED_HOSTS.includes(parsed.hostname))
return res.status(400).json({ error: 'Host not allowed' });
const response = await fetch(req.body.url);
res.json({ preview: (await response.text()).slice(0, 500) });
});What to look for: fetch(), axios.get(), http.get(), or curl-like calls where the URL comes from req.body, req.query, or req.params without hostname validation or allowlisting.
Summary
| Category | Most common AI pattern | Severity |
|---|---|---|
| A01 — Broken Access Control | Missing ownership check on resource lookup | Critical |
| A02 — Cryptographic Failures | Hardcoded secrets, InsecureSkipVerify | Critical |
| A03 — Injection | SQL string concatenation, eval() on input | Critical |
| A04 — Insecure Design | Missing rate limits, no token expiry | High |
| A05 — Security Misconfiguration | Wildcard CORS, debug endpoints | High |
| A06 — Vulnerable Components | Stale pinned versions from training data | Medium–High |
| A07 — Auth Failures | jwt.decode() instead of jwt.verify() | Critical |
| A08 — Data Integrity Failures | pickle.loads() on user-controlled data | Critical |
| A09 — Logging Failures | Silent auth failures, no audit trail | Medium |
| A10 — SSRF | Unvalidated URL fetch from user input | High |
Scan your AI-generated code
CodeSlick checks all 10 OWASP 2025 categories automatically on every PR. 308 named checks across JavaScript, TypeScript, Python, Java, and Go.
Or try it on a live vulnerable repo — 11 AI-generated issues already found.