All posts

AI-Generated Code Security Risks: What Reviewers Should Check

A practical security review for AI-generated code, covering authorization, injection, secrets, dependencies, retries and dangerous infrastructure changes.

AI-generated code can be secure, but clean syntax and a passing test suite do not prove it. Coding agents are especially good at completing the visible path through a feature. Security bugs often live outside that path: the missing ownership check, the retry that charges twice, the URL that lets a user reach an internal service.

Review AI-written code as if it came from a capable contributor who knows the framework but has incomplete knowledge of your threat model. Check every trust boundary against the surrounding application, then use tests and security tools to challenge the assumptions you find.

This guide focuses on the security pass. The broader checklist for reviewing AI-generated code also covers intent, repository conventions, data flow and maintainability.

Why AI-generated code needs a separate security pass

A coding agent usually works from a request such as “add an endpoint that lets users download an invoice.” It can infer routes, database calls and response types. It may not know that invoices belong to workspaces, support staff can impersonate users, object storage URLs must expire after five minutes or audit logs cannot contain billing details.

Those constraints are often scattered across middleware, neighboring endpoints, incident notes and team habits. If the agent did not receive them, the generated implementation can look complete while omitting the control that makes it safe.

The review should answer three questions for every sensitive operation:

  1. Who is acting? Identify the authenticated user, service or background job.
  2. What resource are they touching? Find its owner, tenant and sensitivity.
  3. Where is permission enforced? Point to the exact check, policy or database condition.

If one answer is vague, keep reviewing.

1. Check authorization at the resource boundary

Generated endpoints often check authentication and stop there. A valid session proves who the user is. It does not prove that the user can read or change a particular record.

Consider an endpoint generated from “delete a project by ID”:

export async function deleteProject(projectId: string, userId: string) {
  const user = await database.user.findUnique({ where: { id: userId } })
  if (!user) throw new Error("Unauthorized")

  return database.project.delete({ where: { id: projectId } })
}

Any authenticated user who learns another project ID can delete it. The ownership condition belongs in the query that selects the resource:

export async function deleteProject(projectId: string, workspaceId: string) {
  const project = await database.project.findFirst({
    where: { id: projectId, workspaceId },
    select: { id: true },
  })

  if (!project) throw new Error("Project not found")

  return database.project.delete({ where: { id: project.id } })
}

The real application may also require a workspace role or project permission. The reviewer should be able to follow the authorization decision from the actor to the exact resource.

Look for the same mistake in read endpoints, exports, file downloads, search results and background jobs. Object-level authorization bugs do not need an obviously destructive route.

2. Trace untrusted input to dangerous operations

Find every value controlled by a user, webhook, uploaded file or external API. Follow it until it reaches a database query, shell command, template, filesystem path, redirect or outbound HTTP request.

Pay close attention to:

  • SQL or NoSQL queries assembled from strings
  • shell commands containing request values
  • server-side template rendering
  • archive extraction and file paths
  • URLs fetched by the server
  • redirect destinations
  • regular expressions built from input
  • HTML rendered without escaping

Framework helpers reduce some injection risks, but generated code can step outside them. An agent may use a raw query because it is shorter, call a shell command for an ordinary filesystem operation or disable escaping to make a UI example work.

The OWASP Secure Code Review Cheat Sheet is a useful reference for the sinks and trust boundaries worth tracing. Check its guidance against the framework and versions installed in your repository rather than copying a generic fix.

3. Review outbound requests for SSRF

Features that import a URL, render a preview, test a webhook or fetch an avatar create a server-side request forgery risk. Generated implementations often validate that a string is a URL and assume the job is done.

A safe design usually needs more:

  • allow only the schemes the feature requires
  • resolve and reject loopback, link-local and private network addresses
  • apply the check again after redirects and DNS resolution
  • set response-size and time limits
  • avoid forwarding user credentials or internal headers
  • use an allowlist when the destination set is known

Test decimal, hexadecimal and IPv6 address forms as well as ordinary hostnames. Also test a public hostname that redirects to a private address. URL parsing is the beginning of the control, not the whole control.

4. Inspect secrets, logs and error responses

Agents are often asked to “add useful logging” without being told what the application considers sensitive. The result can include request bodies, authorization headers, session tokens, model prompts, payment details or personal data.

Search the change for:

  • environment variables copied into client bundles
  • full request or response logging
  • secrets embedded in examples and tests
  • access tokens included in URLs
  • database errors returned directly to clients
  • model prompts containing source code or credentials
  • debug endpoints with weak or missing access control

Log identifiers and outcomes where possible, not entire payloads. If a value can authenticate somebody or expose private data, redact it before it reaches logs, traces, analytics and error-reporting services.

Generated examples deserve the same scrutiny as production code. Placeholder credentials have a habit of becoming real credentials during debugging.

5. Verify every new dependency

Language models can suggest a package that is obsolete, unnecessary, misspelled or entirely fictional. A plausible package name is not evidence that the package should enter your build.

For each new dependency:

  1. Confirm the package exists in the official registry.
  2. Open its source repository and release history.
  3. Check that the maintainers and package scope are the expected ones.
  4. Verify the installed version and its known vulnerabilities.
  5. Decide whether the repository already contains a safe way to do the same job.
  6. Inspect install scripts and transitive dependencies when the package handles sensitive data or build infrastructure.

Package hallucination creates an opening for dependency confusion and slopsquatting. Even a legitimate package increases the code and update surface your team owns.

6. Challenge state changes, retries and concurrency

Security failures also come from business logic. A payment endpoint can validate every input and still charge twice when a queue retries. An invitation can expire correctly and still be accepted twice by concurrent requests. A quota can be checked before a write while two requests both observe the old value.

Review important state changes as transitions:

current state + actor + request → permitted next state + side effects

Then ask:

  • Can this operation run twice?
  • Can two actors run it at the same time?
  • What happens after a partial failure?
  • Is the permission checked again when delayed work executes?
  • Can an old link or token be replayed?
  • Does rollback restore every related record?

Use transactions, uniqueness constraints, idempotency keys and state preconditions where the invariant requires them. Do not add all four mechanically. Pick the control that makes the forbidden state impossible or detectable.

7. Treat infrastructure changes as production code

An agent editing a workflow, container or Terraform file can change the security boundary of the whole service. These files often receive less review because they look declarative.

Check for:

  • workflows triggered from untrusted pull-request code with repository secrets available
  • actions pinned only to a mutable tag
  • containers running as root
  • broad cloud IAM permissions
  • public network exposure added for convenience
  • wildcard CORS rules with credentials
  • disabled TLS verification
  • debug flags enabled in production
  • new model or telemetry endpoints receiving repository data

Read the rendered plan or workflow behavior, not only the changed lines. A small configuration edit can grant a large permission.

8. Make security tests disagree with the implementation

Tests generated alongside a feature often repeat the same assumptions as the feature. If the implementation forgets workspace ownership, the generated tests may create only one workspace and never reveal the gap.

Design negative tests independently:

  • authenticated user, wrong tenant
  • valid token, wrong resource
  • expired token replayed
  • duplicate request delivered twice
  • external URL redirecting to an internal address
  • partial failure after the first side effect
  • oversized or malformed input
  • dependency or downstream service unavailable

Temporarily remove the security condition and confirm that the relevant test fails. A green test that survives the return of the bug is decoration. The guide to reviewing AI-generated tests covers mutation checks, over-mocking and shared assumptions in more detail.

A compact security checklist for AI-generated code

Use this during self-review or add the relevant lines to your pull request template:

### Security review for AI-generated code

- [ ] Every sensitive action checks permission on the specific resource.
- [ ] Untrusted input is traced to database, shell, template, file and URL sinks.
- [ ] Outbound requests cannot reach private or link-local services.
- [ ] Logs and errors exclude secrets, tokens, source code and personal data.
- [ ] New packages are real, maintained, necessary and pinned appropriately.
- [ ] Retries, concurrency and partial failures preserve business invariants.
- [ ] Workflow, container and infrastructure permissions are narrowly scoped.
- [ ] Negative tests cover another tenant, replay, duplication and failure paths.
- [ ] A reviewer can explain each security control without asking the coding agent.

Frequently asked questions

Is AI-generated code less secure than human-written code? Not by definition. Its common failure mode is plausible completeness: the code looks finished even when the prompt omitted a security constraint. Review the trust boundaries and test the assumptions instead of judging the code by its origin.

Can an AI code reviewer find security bugs in AI-generated code? It can find useful issues, especially when it sees repository context and explicit security rules. Keep static analysis, dependency scanning, secret scanning and human review in the process. Each catches different failures.

What should I check first in vibe-coded software? Start with authorization, sensitive data and side effects. Identify who can perform each important action, which resource it affects and what happens if the operation runs twice or fails halfway through.

Should AI-generated code be allowed into production? Yes, when the team understands it and it passes the same engineering and security controls as any other change. The person approving the pull request owns the decision, regardless of who typed the code.

For the full review process, use the AI-generated code checklist. If the change is too large to inspect properly, split it into reviewable behavior before approval.

Try Scopy AI on your next pull request

Accurate, open-source AI code reviewer that understands your project. Self-host it or start in the cloud.