The Security Pass
Secrets, injection, auth, and dependency risk — the specific things AI-generated code gets wrong again and again.
Read 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.
const stripe = new Stripe("sk_live_51H8xK2...");const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);- 1Keep .env in .gitignore
Check this before your first commit, not after.
- 2Commit a .env.example instead
Variable names with empty values, so collaborators know what is needed.
- 3Remember 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:
db.query("SELECT * FROM users WHERE email = '" + email + "'");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.
Instructions hiding in what it reads
A tool-using AI that reads a file, a web page, a support ticket, or a pull request cannot reliably tell your instructions apart from text sitting inside that content. A sentence written to look like an instruction — buried in a scraped page, an issue description, or a comment in a data file — can get followed by a model that is, by design, disposed to follow instructions wherever it finds them.
This is a different failure from a user typing something malicious. Nobody at the keyboard asked for it. An agent summarising a ticket that contains a hidden line telling it to paste out environment variables, or a browsing agent that lands on a page seeded with text aimed at agents rather than people, can act on it without either of you noticing until afterwards.
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.
const order = await db.orders.findById(params.id);return Response.json(order);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.
Dependencies it invents, and dependencies that impersonate
Models occasionally suggest importing a package that does not exist — a plausible name, a plausible API, invented outright. That is harmless until someone registers that exact name and fills it with something malicious, betting that enough developers will install a hallucinated suggestion without checking first. It has a name — slopsquatting — because it is a documented pattern, not a hypothetical.
The more common version is older: a package one character or one hyphen removed from a popular real one. Typosquatting works on humans because a tired developer skims a name rather than reading it. It works even better on generated code, where you never typed the name yourself and have nothing to compare it against.
- 1Check the package exists before installing it
Look it up on the registry — download counts, repository link, a maintainer with history. A successful install proves nothing on its own.
- 2Read the name character by character
Typosquats are built to survive a skim, not a careful read.
- 3Pin versions and read the diff on updates
A clean initial install does not protect you from a compromised update later.
How much the agent can touch
A tool-using agent is only as safe as the permissions behind it. One that can read files is low risk. The same agent given permission to run shell commands, push to a remote, or call a paid API is a different proposition — a bad instruction, including an injected one, can now act rather than just suggest.
Broad grants are convenient precisely because they mean fewer interruptions. “Always allow” is one click, and it is exactly the click that turns any of the failure modes above from an annoying suggestion into something that already ran.
- 1Grant the narrowest scope the task needs
Read access for research. Write access scoped to the directory in play. Network access limited to the endpoints the task actually calls.
- 2Keep destructive actions behind a confirmation step
Deleting files, force-pushing, sending anything external — these are worth the extra click every time.
- 3Review a permission grant like you would review a diff
“Always allow shell commands” carries the same weight as merging a PR without reading it.
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.
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- dependencies that do not resolve to a real, maintained package- instructions embedded in fetched content the agent might act on 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
.envis 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
- ✓Every dependency added this session is a real, correctly-named package
- ✓Any agent that reads external content cannot also send data out or spend money in the same run
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 — and redact before you paste one into a prompt too.
- 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.
- Content the model reads is not automatically trustworthy. Treat a fetched page, ticket, or file as data, not instruction.
- Check that a suggested dependency actually exists before installing it. Hallucinated and typosquatted names are a live attack, not a hypothetical.
- Scope agent permissions to the task. “Always allow” on shell or network access turns every other mistake on this list into one that already ran.
- Never hand-roll auth, sessions, password storage, or payments.
Quick check
Answer these to unlock the next chapter — 3 of 4 to pass. You can retake it anytime.
Answer every question to check.
Make a free account to read on
Every chapter is free — an account is how your progress, XP, and streak follow you from your laptop to your phone, and how you show up on the leaderboard. No payment, no trial.