Back to blog
Sep 08, 2026
8 min read

AI writes the code fast, but the security is still your job

AI-generated code looks finished and often is not safe. Recent analysis found nearly half of it ships an OWASP Top 10 flaw. This is the beginner's security baseline: the handful of things you have to check yourself, because the AI will not add them for you.

The fun of building with AI is that a working feature appears in minutes. The trap is that the code looks finished, reads reasonably, and is quietly insecure. This is not a small effect. A 2026 analysis found that around 45% of AI-generated code samples introduced an OWASP Top 10 vulnerability, and the number of security bugs traced directly to AI-written code has been climbing month over month. The speed is real. The safety is not automatic, and closing that gap is a skill you add on top, the same way the rest of this blog treats fundamentals. Here is the baseline.

AI writes insecure code that looks perfect

The reason this catches people is that insecure code does not look insecure. It runs, it passes the happy path, it reads like something a competent developer wrote. The vulnerability is in what it left out: a missing ownership check, an input trusted that should not have been, a permission left wide open. A model optimises for “code that works”, and “works” and “safe” are not the same target.

Two habits from the AI roadmap apply directly. Treat the model like a fast junior developer whose work you review, and never let it run unsupervised against anything real. Everything below is what you are reviewing for.

The one at the top of every list: who gets to see what

Broken access control is the number one risk on the OWASP list, and it is the one AI code gets wrong most quietly. It means a logged-in user can reach data or actions that should not be theirs: opening /orders/1043 when order 1043 belongs to someone else, and the server just hands it over.

The mental model is a nightclub. Checking ID at the door is authentication: are you who you say you are. Deciding which rooms you may enter is authorization: what you are allowed to do. Most beginners get the door right and forget the rooms. Every request for a specific thing has to check that this user owns that thing.

flowchart TD
  R[Request: GET /orders/1043] --> A{Logged in?}
  A -->|no| Deny[Reject: 401]
  A -->|yes| O{Does this user
  own order 1043?}
  O -->|no| Deny2[Reject: 403]
  O -->|yes| Allow[Return the order]

CAUTION

The rule is deny by default. Access is refused unless something explicitly allows it, not allowed unless something blocks it. AI-generated endpoints routinely check that you are logged in and forget to check that the thing you asked for is yours. That single missing check is the most common serious hole in a beginner app.

Secrets, and the mistake that ends up on GitHub

Your API keys, database passwords, and tokens are money and access in text form. The rule is simple and absolute: they never go in the code. They live in environment variables, loaded from a .env file that is in .gitignore from the very first commit, and for anything real, in a secrets manager that can rotate them.

The classic disaster is a key committed to Git. Once it is in the history it is there forever, even if the next commit deletes it, and bots scan public repositories for exactly this within minutes. This is also a factor from the Twelve-Factor App: config and secrets belong in the environment, not the codebase.

WARNING

AI assistants have a specific bad habit here: to make an example run, they will paste a real-looking key straight into the code. Every time an AI hands you code with a key in it, move that key to .env before you do anything else. And 60% of developers ship AI-generated code without tightening the permissions it was given, so check that a key can do only what this feature needs.

Never trust anything the browser sends you

Every piece of data that arrives from a user is untrusted until you check it: form fields, URL parameters, headers, uploaded files, all of it. Validate at the boundary, the moment it enters your server, and prefer a whitelist (“only these values are allowed”) over trying to block bad ones.

This one rule prevents a whole family of attacks called injection, where a user sends input crafted to be run as code or as a database command. The fix is not clever filtering; it is never mixing user input into a command as text. Use parameterised database queries, validate the shape with a schema, and reject anything that does not fit.

// never trust req.body directly
const schema = z.object({ email: z.string().email(), age: z.number().min(0).max(120) })
const result = schema.safeParse(req.body)
if (!result.success) return res.status(400).json({ error: "bad input" })
// only now is result.data safe to use

IMPORTANT

Validation on the front end is for user friendliness, never for security. Anyone can skip your pretty form and call your API directly. The real check has to live on the server, every time. “The front end already checks it” is how the hole gets in.

Do not invent your own login

Authentication is the one place where writing it yourself, or letting an AI improvise it, goes wrong most often. Passwords are never stored as-is; they are hashed with a slow, purpose-built algorithm like argon2id or bcrypt, so a stolen database does not hand over everyone’s password. Add multi-factor authentication for anything that matters. Sessions and tokens need to expire and be revocable.

The professional move is to not build this from scratch. Use a mature auth library or a hosted provider that has already been attacked by everyone and survived. Rolling your own login is the classic overconfident beginner mistake, and an AI will happily write you a broken version that looks convincing.

The library you never checked

Modern apps are mostly other people’s code, and each dependency is a door. Two things to do. Run npm audit (or a tool like Snyk or Socket) so you know when a library you use has a known vulnerability, and keep dependencies updated, ideally with an automated tool that opens the update for you.

There is a new, AI-specific version of this. Models sometimes invent package names that do not exist, and attackers have started registering those exact hallucinated names with malicious code inside, waiting for someone to paste the AI’s suggestion and install it. So when an AI tells you to install something, check the package is real and widely used before you run it.

The baseline, in one checklist

RiskThe habit that prevents itReach for
Broken access controlCheck ownership on every request; deny by defaultYour framework’s auth middleware
Secrets in codeKeep them in .env and a secrets manager; never commitdotenv, Doppler, Vault
InjectionValidate at the boundary; parameterised queriesZod, an ORM (Prisma, Drizzle)
Weak passwordsHash with argon2id/bcrypt; add MFAA mature auth library or provider
Vulnerable dependenciesAudit and auto-update; verify AI package namesnpm audit, Snyk, Socket, Dependabot
Unreviewed AI codeReview it like a junior’s; scan it automaticallyCodeQL, Semgrep, an AI reviewer

AI writes it fast, you make it safe

The lesson is the blog’s whole point, sharpened by a real number: AI makes building fast, and about half of what it writes carries a known security flaw. That is not a reason to stop using it. It is the reason security is a skill you keep, not a feature you assume. The baseline is small and it barely changes year to year: control who can access what, keep secrets out of the code, trust no input, do not hand-roll auth, and watch your dependencies. Run every AI-written feature past that list before it goes live, and you get the speed without shipping the hole.

Let the AI write it. You are still the one who signs off that it is safe.

Sources

Read next