Part 5 of 7 · Chapter 3 of 4

The Security Pass

Secrets, injection, auth, and dependency risk — the specific things AI-generated code gets wrong again and again.

Advanced15 min read

Worth reading first: Reviewing AI-Generated Code


AI-generated code fails at security in a specific and predictable way: it writes the version that works, and security is mostly about the versions that do not. This chapter is the pass you run before anything reaches real users.

Secrets: the one that actually happens

More projects are compromised by a committed API key than by anything clever. It happens because the fastest working version puts the key in the code.

What you will be handed
const stripe = new Stripe("sk_live_51H8xK2...");
What it should be
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
  1. 1
    Keep .env in .gitignore

    Check this before your first commit, not after.

  2. 2
    Commit a .env.example instead

    Variable names with empty values, so collaborators know what is needed.

  3. 3
    Remember that git never forgets

    A key committed and then deleted is still in the history. Rotate it — deleting the line is not enough.

Treat every input as hostile

Generated code assumes input is well-formed, because in the happy path it is. Two consequences dominate.

Injection. Anywhere user text gets concatenated into a query, a command, or HTML:

Vulnerable
db.query("SELECT * FROM users WHERE email = '" + email + "'");
Parameterised
db.query("SELECT * FROM users WHERE email = $1", [email]);

Validation on the server. Client-side checks are a convenience for honest users, not a control. Anyone can call your endpoint directly, and the AI will rarely add server-side validation unless you ask.

Authentication is not authorisation

This is the subtle one, and the one that produces real breaches. The model reliably checks that someone is logged in. It frequently forgets to check that they are allowed to touch this particular record.

Logged in, but whose order is it?
const order = await db.orders.findById(params.id);return Response.json(order);
Ownership checked
const order = await db.orders.findById(params.id);if (order.userId !== session.user.id) {  return new Response("Not found", { status: 404 });}return Response.json(order);

Change the id in the URL and see what happens. That single test finds this class of bug faster than reading ever will.

The review prompt

Ask for a security review as a separate pass, in a fresh conversation. Asking the same thread that wrote the code tends to produce agreement with itself.

Prompt
Review this code for security problems. Check specifically: - secrets or credentials in source- unvalidated user input reaching a query, command, or the DOM- endpoints that check authentication but not ownership- errors that leak stack traces or internal details to users- anything that trusts data from the client For each issue: what an attacker does, and the fix.If you find nothing in a category, say so explicitly.

Before anything reaches real users

The short list

  • No secrets in the repo, and .env is gitignored
  • Every database query is parameterised
  • Every mutating endpoint validates its input on the server
  • Every endpoint that returns a record checks who owns it
  • Error responses say “something went wrong”, not a stack trace
  • Dependencies installed today were checked with npm audit

Key takeaways

  • The model writes the version that works. Security is about the versions that do not.
  • Secrets in source is the failure that actually happens. Gitignore .env before the first commit.
  • Parameterise queries and validate on the server — client checks are convenience, not control.
  • Authentication is not authorisation. Checking login without checking ownership is the classic breach.
  • Run the security review in a fresh conversation, not the one that wrote the code.
  • Never hand-roll auth, sessions, password storage, or payments.