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
| Risk | The habit that prevents it | Reach for |
|---|---|---|
| Broken access control | Check ownership on every request; deny by default | Your framework’s auth middleware |
| Secrets in code | Keep them in .env and a secrets manager; never commit | dotenv, Doppler, Vault |
| Injection | Validate at the boundary; parameterised queries | Zod, an ORM (Prisma, Drizzle) |
| Weak passwords | Hash with argon2id/bcrypt; add MFA | A mature auth library or provider |
| Vulnerable dependencies | Audit and auto-update; verify AI package names | npm audit, Snyk, Socket, Dependabot |
| Unreviewed AI code | Review it like a junior’s; scan it automatically | CodeQL, 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
- OWASP Top 10 (2026): the most critical web application security risks for broken access control at number one and the stable fundamentals
- OWASP input validation cheat sheet for validating at the boundary and whitelisting
- Cloud Security Alliance: the AI-generated code vulnerability surge (2026) for the CVE surge and the 45% figure
- Cycode: top AI security vulnerabilities to watch in 2026 for unadjusted permission scopes and prompt injection in AI workflows