A practical checklist for reviewing AI-generated code before it ships: injection, broken access control, hardcoded secrets, and other vulnerability classes to check.
AI coding assistants routinely produce code that runs correctly on the first try, and that is exactly the trap. Running correctly and being secure are different properties, and an assistant trained on public code has absorbed both the safe patterns and the insecure ones sitting right next to them in the training data. When a model needs to build a database query, hash a password, or parse an uploaded file, it draws on the same distribution that includes string-concatenated queries, weak hashing calls, and unchecked deserialization.
This is not a defect specific to any one model or vendor. It is a structural consequence of pattern completion: the assistant optimizes for code that works, not code that resists an attacker, and nothing in a typical prompt asks it to think adversarially. A security review of AI-written code has to start from that assumption and check a specific, bounded list of vulnerability classes before anything ships, the same way you would check a junior engineer's first pull request against a known list rather than trusting that it reads cleanly. This post covers what to check, how to check it, and how to run that check as its own dedicated pass rather than folding it into an ordinary code review.
Why AI-written code needs a dedicated security pass
General code review is tuned to catch logic errors, unclear naming, and structural problems, and reviewers scanning for those things tend to pass over a query built with string interpolation if the surrounding code reads cleanly and the feature works. Security bugs are camouflaged: they look like ordinary code until someone asks a specific adversarial question, such as what happens if this input is hostile, and that question does not arise naturally while confirming a feature behaves correctly. AI assistants also tend to be consistent within a session, so one insecure pattern chosen early in a feature often gets repeated across every similar call site, which means a single miss in review can multiply into a dozen instances before anyone notices.
Six vulnerability classes to check before shipping
- Injection (SQL, shell, template): watch for queries, commands, or rendered templates built by concatenating or interpolating user input instead of using parameterized queries, an argument-array call, or an auto-escaping template engine.
- Broken access control and IDOR: any handler that looks up a record by an ID from the request needs to filter on the requesting user's ownership or role too, not just the ID. List and search endpoints need the same filter as single-record lookups.
- Hardcoded secrets: API keys, tokens, and connection strings left as literals in source, test fixtures, or example configs, usually because a config-loading step was not wired up yet when the code was generated.
- Unsafe deserialization: untrusted input passed to a deserializer that can instantiate arbitrary objects, such as unsafe YAML loaders, pickle, or eval-based parsing, which can lead to code execution rather than just malformed data.
- Missing input validation: code that assumes a field is present, correctly typed, or within range because the example it generalized from never needed to check, so nothing enforces those assumptions at the boundary.
- Weak authorization ordering: permission checks that run after the action instead of before it, or that exist on one code path but were never copied to a newer or duplicate one.
None of these six require exotic knowledge to catch. They require asking a specific question of specific code, which is different from reading a diff and confirming it does what the ticket asked. Treat the list as a minimum, not a ceiling: a codebase with file uploads, background jobs, or third-party webhooks accumulates its own additional classes worth adding, but these six show up often enough in AI-generated code that skipping any one of them on a review is a choice, not an oversight.
| Vulnerability class | What to look for |
|---|---|
| Injection (SQL/shell/template) | String-built queries, shell calls, or templates instead of parameterized queries, argument arrays, or auto-escaping |
| Broken access control / IDOR | Lookups filtered by ID alone; list endpoints missing the ownership filter single-record lookups have |
| Hardcoded secrets | Keys, tokens, or connection strings as literals in source, tests, or example config files |
| Unsafe deserialization | Untrusted input reaching pickle, unsafe YAML loaders, eval, or object-instantiating parsers |
| Missing input validation | No type, presence, or range check at the request boundary before business logic runs |
| Weak authz ordering | Permission checks after the action, or absent on a duplicate or newer code path |
Grep the diff for raw string concatenation feeding into query, command, or template calls, and separately for every place a resource ID from the request reaches a database lookup. Those two searches alone surface a large share of AI-introduced vulnerabilities in a typical feature branch.
If a vulnerability class is not a specific line item on your review checklist, treat it as unchecked. A general instruction like review this for security produces inconsistent results, because it never forces the specific adversarial question for each class to actually get asked.
Running a dedicated adversarial pass
The fix is not more code review, it is a different kind of review run at a different time. A dedicated adversarial pass treats the finished code the way an attacker would: given full knowledge of the implementation, where would you inject, whose data would you request by changing an ID, which deserializer would you feed a hostile payload. This works best as a separate step after functional correctness is already established, since evaluating whether code works and whether it can be exploited in the same pass means the harder adversarial question gets rushed the moment the functional test passes. This is the same principle behind TLM Forge's red-team gate: functional review and adversarial review are deliberately separated, the adversarial pass is scoped to hunt exactly the classes above against the actual diff rather than the plan, and nothing ships while a critical finding is open. See how it works for how the stages fit together, or compare it against plan-only review processes. In practice a gate like this needs a severity threshold and a hard stop, not a suggestion: a finding in one of the six classes above should block a merge the same way a failing test does, and the reviewer running the pass should be looking at the actual code that will ship, not a description of the plan or an earlier draft, since the vulnerable line can move, change shape, or get reintroduced between the plan and the final diff.
Carry the review forward
A checklist only helps if it gets applied the same way every time, and consistency is hard to maintain across a long project where context resets between sessions. An assistant that reviewed a file for injection last week has no memory of that review today, which is part of why a fixed bug can reappear later in a different function that copies the original pattern, or why a secret that was flagged and removed once quietly comes back in a new config file six weeks later. MemX, a private, persistent memory layer for AI tools, addresses a related problem: keeping project context, including what was already reviewed and flagged, available across sessions instead of starting from zero every time. Whatever carries that context forward, the review itself should stay mechanical: the same six classes, checked the same way, on every change that ships, not only the ones that feel risky. See common mistakes with AI coding tools and red-teaming your AI code for related patterns worth building into that default flow.
Frequently asked questions
01Does reviewing AI-written code for security need different steps than reviewing human-written code?
The vulnerability classes are the same ones any code review should check. What changes is the base rate: AI assistants reproduce insecure patterns from training data consistently across a codebase, so a dedicated pass that explicitly checks each class catches more than a general review, because the resulting bugs look like ordinary code rather than obvious mistakes.
02Can the assistant that wrote the code catch these issues if you ask it to review its own output?
Sometimes, but a self-review by the same model that generated the code tends to inherit the same blind spots that produced it. An independent adversarial pass, ideally a separate step or a different reviewer, is more reliable than asking the original generation to grade itself.
03Where does a security pass fit relative to functional testing?
After functional correctness is established, not combined with it. Checking whether code works and whether it can be exploited in the same pass means the adversarial question tends to get skipped once the functional test passes. Running it as its own dedicated step, the way red-teaming your AI code describes, keeps it from being dropped under time pressure.