← All posts

AI code security

AI-generated code vulnerabilities: the patterns AI tools ship most

AI coding tools don't produce random bugs. They produce the same handful of vulnerability classes, repeatedly, across completely unrelated codebases. Here's what they are, with real code examples.

By Prbl Security Team

General-purpose security scanners are built to catch a broad spectrum of bugs. AI-generated code has a narrower, more predictable failure surface, because the models making the mistakes are the same models, making the same tradeoffs, every time they’re asked to move fast. In our scan of 976 real AI-built apps from GitHub, 1 in 8 had at least one high-severity finding. The patterns are consistent enough that we can describe them precisely.

Hardcoded credentials

AI tools frequently embed real-looking credentials directly in generated files, especially database migrations, seed scripts, and config files, instead of reading them from environment variables. The model optimizes for a working result and inlining the credential is the shortest path.

// VULNERABLE: secret hardcoded in source — in git history forever
const stripe = new Stripe('sk_live_4eC39HqLyjWDarjtT1zdp7dc');

// Also vulnerable: predictable fallback if env var is unset
const jwtSecret = process.env.JWT_SECRET || 'dev-secret-change-me';
// ^ if JWT_SECRET is missing in production, any attacker can forge tokens

// CORRECT: fail loudly if the secret is missing
const jwtSecret = process.env.JWT_SECRET;
if (!jwtSecret) throw new Error('JWT_SECRET environment variable is required');

The fallback pattern is especially common and especially dangerous: it passes every local test and only fails when someone deploys without setting the variable.

Scan your own app for issues like these

Paste your live URL. We check what your app serves publicly for exposed keys and misconfigurations. No account, no install.

Missing access control on generated routes

AI tools are excellent at scaffolding working CRUD endpoints. They frequently omit the authentication check entirely, or check that a user is logged in without checking that they own the specific resource they’re requesting. The second case is Broken Object Level Authorization (BOLA), and it is the most common access control gap in AI-generated APIs.

// VULNERABLE — no auth check at all
app.get('/api/invoices/:id', async (req, res) => {
  const invoice = await db.invoice.findById(req.params.id);
  res.json(invoice); // returns any invoice to any caller
});

// ALSO VULNERABLE — auth check but no ownership check (BOLA)
app.get('/api/invoices/:id', authenticate, async (req, res) => {
  const invoice = await db.invoice.findById(req.params.id);
  // user is logged in, but this returns ANY user's invoice
  res.json(invoice);
});

// CORRECT — both auth and ownership
app.get('/api/invoices/:id', authenticate, async (req, res) => {
  const invoice = await db.invoice.findOne({
    _id: req.params.id,
    userId: req.user.id, // must belong to this user
  });
  if (!invoice) return res.status(404).json({ error: 'Not found' });
  res.json(invoice);
});

Injection from string concatenation

Parameterized queries take more tokens to write than string concatenation, and models optimizing for a fast response reach for string interpolation even in a codebase that uses parameterized queries everywhere else. The pattern appears in SQL, MongoDB filters, and shell commands.

// VULNERABLE: SQL injection
const result = await db.query(
  "SELECT * FROM users WHERE email = '" + req.body.email + "'"
  // attacker sends: email = "' OR '1'='1" to dump all users
);

// CORRECT: parameterized query
const result = await db.query(
  'SELECT * FROM users WHERE email = $1',
  [req.body.email]
);

// VULNERABLE: MongoDB NoSQL injection via unvalidated object
const user = await User.findOne({ email: req.body.email });
// attacker sends: email = { "$gt": "" } to match any user

// CORRECT: validate that email is a string before querying
const email = String(req.body.email);
const user = await User.findOne({ email });

JWT tokens decoded without verification

AI tools often generate code that reads a JWT’s payload with jwt.decode() or the jwt-decode library to extract a user ID or role. Decoding does not check the signature — anyone can create a token with any claims they want. Using a decoded field to make an authorization decision is a bypass waiting to happen.

// VULNERABLE: trusts the payload without checking the signature
const payload = jwt.decode(token); // no secret needed = no verification
if (payload.role === 'admin') allowAccess();

// CORRECT: verify() checks the signature and throws on tampered tokens
const payload = jwt.verify(token, process.env.JWT_SECRET, {
  algorithms: ['HS256'],
});
if (payload.role === 'admin') allowAccess();

Full explanation: JWT decode vs verify.

Timing-unsafe comparisons

AI tools often generate signature === computed instead of a constant-time comparison for webhook verification. A timing attack can exploit the difference in comparison time to determine the secret character by character. It is a subtle bypass vector that looks correct in code review.

// VULNERABLE: timing-unsafe — === returns early on first mismatch
const sig = req.headers['x-stripe-signature'];
const computed = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
if (sig !== computed) return res.status(403).send('Invalid');

// CORRECT: constant-time comparison
const sigBuf = Buffer.from(sig, 'hex');
const computedBuf = Buffer.from(computed, 'hex');
if (sigBuf.length !== computedBuf.length || !crypto.timingSafeEqual(sigBuf, computedBuf)) {
  return res.status(403).send('Invalid');
}

// Or just use the official library's verification
stripe.webhooks.constructEvent(rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET);

Why volume is the real risk

Each of these issues individually looks minor in a diff. The risk is volume: a codebase with dozens of AI-generated files accumulates dozens of these small, repeatable mistakes, and most teams aren’t reviewing AI output any more carefully than human output, often less. A scanner running on every pull request catches the automatable categories before they accumulate.

Frequently asked questions

What are the most common AI-generated code vulnerabilities?

In order of frequency from our scan of 976 real AI-built repos: (1) hardcoded API keys and credentials, (2) missing server-side authentication on API routes, (3) fallback secrets in environment variable lookups (process.env.SECRET || 'default'), (4) JWT tokens decoded without signature verification, (5) SQL and NoSQL injection from string concatenation, and (6) Broken Object Level Authorization — checking that a user is logged in but not that they own the specific resource. The first three account for most high-severity findings.

Is AI-generated code less secure than hand-written code?

Yes, measurably. In our scan of 976 real AI-built apps, 1 in 8 had at least one high-severity finding. The distribution of vulnerabilities is also different: AI-generated code has a higher concentration of hardcoded secrets, missing authorization, and fallback credentials than hand-written code, because the model optimizes for a working result and those shortcuts unblock integrations without breaking functionality.

Why do AI tools generate hardcoded credentials?

AI coding tools optimize for a working result in as few steps as possible. Inlining a credential is the shortest path to a running integration — it avoids the need to explain environment variable setup, create a .env file, update .gitignore, and add a startup check. The model's training data also contains a lot of tutorial code that hardcodes credentials for simplicity, so it reproduces that pattern.

What is BOLA and why does it appear so often in AI-generated code?

BOLA (Broken Object Level Authorization) means your route checks that a user is logged in but not that the requested object belongs to them. AI tools generate the authentication check because it is required to make the happy path work, but they omit the ownership check because the feature still functions without it — it just exposes other users' data. It is the most common access control gap in AI-generated APIs.

What is a fallback secret and why is it dangerous?

A fallback secret is a pattern like process.env.JWT_SECRET || 'dev-secret-change-me'. In local development it works fine — the variable is unset, so the fallback kicks in and the app runs. In production, if the environment variable is missing (a misconfiguration, a missed deployment step, a new server), the app silently falls back to the predictable default instead of failing. Anyone who knows or guesses the default can forge authentication tokens.

Does running a security scanner catch all AI-generated vulnerabilities?

A scanner catches the automatable classes well: hardcoded secrets, injection patterns, known-bad cryptography patterns. It misses authorization bugs that require understanding business logic — whether a user should be able to read a specific record is something a scanner can only partially reason about. The combination of a secrets and pattern scanner plus a manual check of every API route's authorization logic covers the main categories.

Ready to check your own app?

Paste your live URL. We check what your app serves publicly for exposed keys and misconfigurations. No account, no install.

Or see a live example scan first.

Prbl in one place: SAST for AI code · secret scanner · website vulnerability scanner · AI code review · GitHub security scanner · OWASP Top 10 · open dataset

AI-Generated Code Vulnerabilities: The Patterns AI Tools Ship Most | Prbl