Reviewing AI-Generated Code for Security Flaws
Generated code often looks right and fails at the edges. A short security checklist catches the problems that recur.

On this page
A function written in a few seconds reads as if someone thought about it for an hour. The naming is sensible, the structure is familiar and the comments are tidy. That polish is the reason generated code needs a deliberate security review, because the flaws it carries are rarely the ones that look wrong on a first read.
Why generated code fails differently
A model writes code by continuing patterns it learned from a large body of existing code. That body contains a great deal of working code, and also a great deal of code that was insecure, outdated or written only to demonstrate an idea. The output tends to be fluent and idiomatic, which makes it easy to accept. The failures tend to sit at the edges: missing checks, unusual inputs and cases the prompt never mentioned.
A second difference is that the model rarely has your security requirements in front of it unless you wrote them down. If the prompt asked for a handler that returns an order, the model will return an order. Whether the caller is allowed to see that order is a question nobody asked. Generated code also tends to cover the happy path, so the reviewer has to reason about the paths the author never considered.
Tests deserve the same suspicion. Tests written by the same tool often share its assumptions, so a suite that passes may only confirm that the code does what the author expected. A passing suite is useful evidence about behavior and weak evidence about security, because security properties are usually the cases nobody thought to write.
Keep the reviewer's stance steady. The question is not whether the author meant well. It is whether the code does the right thing for every input it can receive.
Check authorization before anything else
The most common serious flaw in generated handlers is not an exotic injection. It is a missing ownership check. The code verifies that a user is logged in, fetches a record by the identifier in the request and returns it. Nothing confirms that the record belongs to the person asking. Any identifier the caller can guess or obtain then opens another person's data.
Review every endpoint for two things: who may call it, and which objects that caller may reach. The object check belongs close to the data access, ideally in a shared function rather than repeated by hand in each handler. Write a test that signs in as one user and requests another user's record. The test should fail, and it should keep failing after future changes.
Input handling and output
Validate input at the boundary, using a defined schema that rejects anything outside it. Use parameterized queries for every database call, including calls where the value looks safe. Avoid building shell commands by joining strings with user input. Avoid building file paths from request values unless the path is normalized and checked against an allowed directory.
Output needs the same care. Text rendered as HTML must be encoded for that context. Data passed to another interpreter, such as a template engine or a query language, should go through that interpreter's escaping rules rather than through hand-written ones. Deserializing data from untrusted sources is a separate risk, so prefer formats that carry data only, and avoid loaders that can construct arbitrary objects.
Dependencies, defaults and secrets
Generated code often names libraries. Check that each one exists, is maintained and is the official package rather than a lookalike. Models sometimes name packages that do not exist, and a name that does not exist can be registered by someone else. Pin versions, and run a dependency audit as part of the build.
Generated examples frequently carry insecure defaults: certificate checks turned off to make a test pass, cross-origin rules set to allow everything, debug modes left on, or keys written directly into the source. Search for these patterns explicitly. Secret scanners catch many hardcoded keys, but they do not know which defaults are dangerous in your context, so the reviewer has to read the configuration as well as the code.
A routine and the tools that help
A reasonable routine has four parts. Run static analysis to catch common patterns. Run a dependency audit. Check authorization on each endpoint against a written list of who may call it. Then write tests for the security properties that matter: a user cannot read another user's record, a malformed request is rejected, and an error message does not reveal internal paths.
Review the diff as well as the finished file. Generated changes often touch code around the requested feature, such as a shared helper, a configuration value or an import, and those side edits are the ones a reviewer is least likely to read closely.
Tools help with pattern matching across a large codebase. They do not know your authorization model, and they miss flaws that depend on the meaning of a business rule. Treat them as a first pass that narrows where a person should look, not as a verdict. Where a tool and a reviewer disagree, record the disagreement, because it shows what the tool cannot see.
Practice on one handler
Choose a handler from your own project, or write a generated sample for practice. Review it against the four parts above and record every finding with its location. Then write two tests for security properties the handler should satisfy, and run them. Note which of your findings a scanner also flagged and which it missed. The gap between those two lists is the part of the review that depends on understanding the system, and it is where your effort is best spent.

