TL;DR
- AI editors write protected routes with zero auth middleware
- jwt.decode() is not jwt.verify() -- the difference lets attackers forge tokens
- Login endpoints with no rate limiting can be brute-forced in seconds
I've been reviewing AI-generated Node.js backends for the past couple of months. The vulnerability that keeps appearing isn't SQL injection or hardcoded secrets. It's broken auth. Three specific patterns, repeated across every project I've looked at.
None of these are flashy. The code runs fine. It just runs insecurely.
No auth middleware on protected routes (CWE-306)
Cursor writes this constantly:
// bad -- CWE-306: Missing authentication
app.get('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
res.json(user);
});
No middleware. No token check. Any unauthenticated request returns the full user record.
The AI saw thousands of tutorial routes that skip auth to focus on the main teaching point. It learned the default pattern.
The fix adds an authenticate middleware and an ownership check:
const authenticate = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Unauthorized' });
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch {
return res.status(401).json({ error: 'Invalid token' });
}
};
app.get('/api/users/:id', authenticate, async (req, res) => {
if (req.user.id !== req.params.id) return res.status(403).json({ error: 'Forbidden' });
const user = await User.findById(req.params.id);
res.json(user);
});
The ownership check matters. Without it you fix the auth problem and introduce IDOR instead.
jwt.decode() is not jwt.verify() (CWE-347)
This one is subtle. AI often generates token handling code that reads the payload but skips signature verification:
// bad -- CWE-347: Improper verification of cryptographic signature
const payload = jwt.decode(token);
const userId = payload.sub;
jwt.decode() does zero cryptographic verification. It just base64-decodes the payload. An attacker can craft a token with any sub value they want, send it, and the application accepts it.
The fix is one word:
// good
const payload = jwt.verify(token, process.env.JWT_SECRET);
const userId = payload.sub;
jwt.verify() throws if the signature is invalid. That exception gets caught in your middleware and returns 401.
No rate limiting on login endpoints (CWE-307)
Every AI-generated login endpoint I've seen looks like this:
// bad -- CWE-307: No brute-force protection
app.post('/api/auth/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
const match = await bcrypt.compare(password, user.password);
if (match) res.json({ token: generateToken(user) });
else res.status(401).json({ error: 'Invalid credentials' });
});
Unlimited attempts. Even with bcrypt adding latency per check, a targeted attack against a known email can run through a wordlist in minutes.
Two lines of change:
// good
import rateLimit from 'express-rate-limit';
const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 10 });
app.post('/api/auth/login', loginLimiter, async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
const match = await bcrypt.compare(password, user.password);
if (match) res.json({ token: generateToken(user) });
else res.status(401).json({ error: 'Invalid credentials' });
});
Why this keeps happening
AI editors train on code from tutorials, blog posts, and StackOverflow. Tutorials skip auth middleware to focus on the main point. Answers strip code down to the minimum viable snippet. The model absorbed millions of examples where middleware was left out.
It's not a flaw in the model's understanding of auth. It's a gap in the training distribution. Security patterns are underrepresented, and the model fills in defaults from whatever was most common in the data.
The fix is not better prompts. It's a scanner that runs at the moment code is generated.
I've been running SafeWeave for this. It hooks into Cursor and Claude Code as an MCP server and flags missing auth middleware, jwt.decode() misuse, and unprotected endpoints before I move on. That said, even a basic pre-commit hook with semgrep will catch most of what's in this post. The important thing is catching it early, whatever tool you use.
Top comments (0)