You built your MVP with Claude, Cursor, Lovable, or Copilot. It works. It's fast. It ships.
Then reality hits: 196 out of 198 AI-generated apps contain security vulnerabilities (Endor Labs, 2025). Not "might contain." Contain.
Here's the uncomfortable truth: AI coding assistants are trained on decades of vulnerable code from GitHub, Stack Overflow, and public repositories. They replicate those patterns at scale—not out of malice, but because they're statistically likely. A model that generated perfectly secure code 100% of the time would have to unlearn everything it learned during training.
This isn't a flaw in AI. It's a gap in how we use it.
Below are the 10 most dangerous security flaws AI tools introduce into production code, why they happen, and how to fix them.
The most common flaw, and the easiest to miss. AI-generated endpoints often accept and trust anything the client sends.
// AI-generated API endpoint
app.post('/api/users', (req, res) => {
const user = {
email: req.body.email,
role: req.body.role, // ← AI accepts whatever role the request sends
isAdmin: req.body.isAdmin // ← Should NEVER come from user input
};
db.insert('users', user);
res.json({ success: true });
});
app.post('/api/users', (req, res) => {
// Validate email format
if (!req.body.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(req.body.email)) {
return res.status(400).json({ error: 'Invalid email' });
}
// Whitelist allowed roles
const allowedRoles = ['user', 'moderator'];
if (!allowedRoles.includes(req.body.role)) {
return res.status(400).json({ error: 'Invalid role' });
}
// Never allow isAdmin from user input
const user = {
email: req.body.email,
role: req.body.role,
isAdmin: false // ← Always set server-side
};
db.insert('users', user);
res.json({ success: true });
});
The classic vulnerability, still alive and thriving in AI-generated code. String interpolation in queries is an open door for attackers.
const userId = req.query.id;
const query = `SELECT * FROM users WHERE id = ${userId}`;
db.query(query);
// Attacker sends: id=1 OR 1=1 — returns entire users table
// Or: id=1; DROP TABLE users; — deletes everything
// Use parameterized queries — ALWAYS
const userId = req.query.id;
const query = 'SELECT * FROM users WHERE id = $1';
db.query(query, [userId]); // Parameter passed separately, never interpolated
One of the most dangerous flaws. API keys, passwords, and tokens embedded directly in source code get committed to Git — and attackers scan GitHub 24/7 looking for exactly this.
const API_KEY = "sk-proj-abc123xyz789def456"; // ← In source code!
const DB_PASSWORD = "prod_password_2024";
const stripe = require('stripe')(API_KEY);
const pool = new Pool({ password: DB_PASSWORD });
// Use environment variables — always
const API_KEY = process.env.STRIPE_API_KEY;
const DB_PASSWORD = process.env.DATABASE_PASSWORD;
if (!API_KEY || !DB_PASSWORD) {
throw new Error('Missing required environment variables');
}
const stripe = require('stripe')(API_KEY);
const pool = new Pool({ password: DB_PASSWORD });
.gitignore:
.env .env.local .env.*.local
AI-generated auth checks are often superficial — checking that a field exists, not that it's actually valid.
app.get('/api/dashboard', (req, res) => {
// Just checks if a username was provided — no password, no token
if (req.body.username) {
res.json({ dashboardData: getAllData() });
} else {
res.status(401).json({ error: 'Unauthorized' });
}
});
app.get('/api/dashboard', authenticateToken, (req, res) => {
const user = req.user; // Set by middleware after real token verification
res.json({ dashboardData: getUserData(user.id) });
});
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) return res.status(401).json({ error: 'No token' });
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) return res.status(403).json({ error: 'Invalid token' });
req.user = user;
next();
});
}
Even when authentication works, authorization is often missing. Any logged-in user can view any other user's data just by changing an ID in the URL.
app.get('/api/users/:id/profile', (req, res) => {
const userId = req.params.id;
const userProfile = db.query('SELECT * FROM users WHERE id = ?', [userId]);
res.json(userProfile);
// ← Returns ANY user's profile to ANY logged-in user
// Visit /api/users/1/profile to get admin's data
});
app.get('/api/users/:id/profile', authenticateToken, (req, res) => {
const requestedUserId = req.params.id;
const currentUserId = req.user.id; // From verified JWT
// Only allow users to access their own profile (or admins)
if (requestedUserId !== currentUserId && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Forbidden' });
}
const userProfile = db.query('SELECT * FROM users WHERE id = ?', [requestedUserId]);
res.json(userProfile);
});
Injecting malicious scripts into your pages through user-generated content. React protects you most of the time — but edge cases break this assumption.
// If comment.text = "<img src=x onerror='stealCookie()'>"
// The script runs in every visitor's browser
function CommentDisplay({ comment }) {
return <div dangerouslySetInnerHTML={{ __html: comment.text }} />;
}
import DOMPurify from 'dompurify';
function CommentDisplay({ comment }) {
// Sanitize HTML before rendering — removes script tags and event handlers
return <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(comment.text) }} />;
}
// Or if you don't need HTML formatting at all:
function CommentDisplay({ comment }) {
return <div>{comment.text}</div>; // React auto-escapes this
}
AI-generated password hashing frequently uses MD5 or SHA-1 — algorithms that were broken over 20 years ago. Some AI code stores passwords in plaintext entirely.
// Using weak/deprecated algorithms
const hashedPassword = crypto.createHash('md5').update(password).digest('hex');
// MD5 has been broken since 2004. Rainbow tables crack these instantly.
// Or even worse — plaintext:
db.insert('users', { email, password: plaintext_password });
const bcrypt = require('bcrypt');
// Hash before storing — bcrypt is slow by design, making brute-force impractical
const hashedPassword = await bcrypt.hash(password, 12); // 12 = work factor
db.insert('users', { email, password: hashedPassword });
// When verifying login:
const isValid = await bcrypt.compare(providedPassword, storedHash);
AI tools generate project scaffolding that often includes .env files, config files with hardcoded values, or .claude directories — and none of it gets added to .gitignore.
# These files committed to public GitHub repos:
/.env # Database URLs, API keys, JWT secrets
/.claude # Claude Code config — may contain credentials
/config.js # Hardcoded database URLs
/scripts/seed.js # Contains production database connection strings
CVE-2026-21852: Attackers could steal API keys by injecting malicious repository configuration.
# .gitignore — add these before first commit
.env
.env.local
.env.*.local
.env.production
config.local.js
*.pem
*.key
secrets/
.claude/
AI generates npm install commands based on tutorials — often referencing packages that are years out of date with known CVEs. The code works. The security holes are invisible.
npm install some-popular-library@1.0.0
# That version from 5 years ago? It has 12 known CVEs.
# AI doesn't check vulnerability databases — it just patterns match.
# Check for vulnerabilities
npm audit
# Auto-fix what it can
npm audit fix
# See what's outdated
npm outdated
# Fail CI/CD builds if high-severity vulns exist
npm audit --audit-level=high
The invisible flaw. When there's no logging, attacks go undetected until after the breach. AI-generated code generates minimal working code — which means zero audit trail.
app.post('/api/login', (req, res) => {
const user = db.query('SELECT * FROM users WHERE email = ?', [req.body.email]);
if (user && passwordMatches) {
res.json({ token: generateJWT(user) });
} else {
res.status(401).json({ error: 'Bad credentials' });
}
// No logging. Brute-force attack against your app? You'll never know.
});
app.post('/api/login', (req, res) => {
const email = req.body.email;
const user = db.query('SELECT * FROM users WHERE email = ?', [email]);
if (!user) {
logger.warn(`Login attempt for non-existent user: ${email} from ${req.ip}`);
return res.status(401).json({ error: 'Invalid credentials' });
}
if (!passwordMatches) {
logger.warn(`Failed login for ${email} from ${req.ip}`);
incrementFailedLoginCount(email); // Trigger lockout after N attempts
return res.status(401).json({ error: 'Invalid credentials' });
}
logger.info(`Successful login: ${email} from ${req.ip}`);
res.json({ token: generateJWT(user) });
});
The Bigger Picture: Why This Keeps Happening
AI makes code faster. It doesn't make code safer.
Think of it like a contractor who builds fast and cuts corners. The house stands. The doors open. But the locks are cheap. You have to inspect the work before you move in.
These ten flaws aren't rare edge cases from obscure models. They appear consistently across Cursor, Copilot, Claude, ChatGPT, and Lovable — because they all learned from the same vulnerable codebase: the internet.
How to Protect Your AI-Generated Code
1. Manual Review Before Deploy
AI-generated code in security-critical paths (auth, payments, data access) requires human eyes. Specifically look for the 10 flaws above. Treat it the same way you'd treat code from a new contractor.
2. Static Analysis (SAST)
Run automated scanners on your code before it ships. SonarQube finds security smells. Snyk catches vulnerable dependencies. Checkmarx specializes in injection flaws. Most have free tiers.
3. Dynamic Testing (DAST)
Attack your own running application. Tools like OWASP ZAP and Burp Suite simulate real attacks against your staging environment — SQL injection, XSS, auth bypasses. Find them before attackers do.
4. Dependency Scanning (SCA)
Run npm audit before every deploy. Pin major dependency versions. Subscribe to security advisories for your top 10 dependencies. One outdated package can compromise everything else.
5. Write Security-Conscious Prompts
Be explicit when using AI tools. Instead of "generate a login endpoint," say: "generate a secure login endpoint using parameterized queries, bcrypt hashing, JWT tokens, input validation, rate limiting, and logging for failed attempts." You get what you ask for.