Skip to content
SecHelixv3 alpha
GitHub
DocsContributeSupportWorkbenchGitHub
Back to overview
Research

How to Security-Review AI-Generated Code

AI-generated code does not fail in new ways. It fails in the usual ways, faster, and in places a reviewer has not read.

Start from the right premise

“Written by AI” is not a vulnerability class. Generated code fails in the same ways hand-written code fails: an authorization check that is present on one route and absent on another, a price the server trusts from the client, a fetch that follows a redirect it never revalidates. Treating it as an exotic new category leads to the wrong review.

Two things are different, and they are both about volume and attention. There is more code per hour than a reviewer has historically had to read, and much of it looks idiomatic — which is exactly the property that makes a missing check hard to see. The related argument, that a control can be the right shape and the wrong property, is on the AI-generated code security page. This page is the practical order.

A review order that finds things

Reviewing in file order finds whatever happens to be near the top. Reviewing in authority order finds the things that matter:

Review order
1. Authorization        who may act on this object, on every path that reaches it
2. Identity and price   which request fields the server re-derives instead of trusting
3. Business invariants  what must stay true across a state machine, and where it is enforced
4. Outbound requests    which URLs the app fetches, and what validates them after a redirect
5. Inbound files        what an upload is trusted to be, and where it is stored and served
6. Secrets              what is in the repository, the client bundle, and the logs
7. Supply chain         what was added, by whom, and whether the lockfile agrees
8. Agent surfaces       which tools exist, who may call them, and what text reaches the model

1 · Authorization, on every path

The recurring shape is not a missing check. It is a correct check on the path a developer was thinking about, and no check on a second path that reaches the same object — a list endpoint that scopes by tenant and an export endpoint that re-fetches by primary key.

  • Enumerate every route that can return or mutate an object, not just the obvious one. Exports, webhooks, admin tools, background jobs and cache-warmers all count.
  • Check the object-level decision, not only the authenticated-user decision. "Is logged in" is not "may act on this record".
  • Where a database policy is the control, confirm the connection actually carries the identity the policy reads. A row-level policy on a connection that never sets the tenant is decoration.

Depth on this class: BOLA and IDOR review.

2 · Client-controlled fields

Anything the client sends is a request, not a fact. The fields that cause real incidents are the boring ones:

FieldWhat the server must do
user_id / tenant_id / account_idDerive from the session. Never read from the body, never trust a header.
price, amount, currency, quantityRe-price server-side from the catalog and the cart. The client sends intent, not totals.
role, plan, is_admin, permissionsLoad from storage. A submitted role is an escalation attempt with good manners.
status / state transitionsValidate against the allowed transition, not merely against the enum.
redirect_uri, next, return_toAllowlist. An open redirect is a phishing primitive and an OAuth problem.

3 · Business invariants

Generated code implements the happy path well and the invariant rarely, because the invariant is usually not written down anywhere the model could read it. A voucher that may be used once, a refund that may not exceed the capture, a seat that may be held by one booking: each needs a place where the rule is enforced, and that place is almost always the database.

Check-then-act is the tell. If the code reads a row, decides, and then writes, ask what happens when two requests interleave — and look for the unique constraint or conditional update that closes the window. Depth: business logic security testing.

4 · Outbound requests and SSRF

Any feature that fetches a user-supplied URL — link previews, webhooks, avatar imports, PDF renderers, “import from URL” — is an SSRF question. The validation is usually present and usually insufficient:

  • Validate after every redirect, not only on the first URL. A 302 to 169.254.169.254 defeats a first-hop check.
  • Deny by allowlist of hosts and schemes rather than by blocklist of addresses. Blocklists miss IPv6, decimal notation, and the second private range someone forgot.
  • Resolve and pin the address you validated, or you have validated one name and connected to another.

5 · Uploads and parsers

Type
Decide from content, not from the filename or the client-sent Content-Type. Then decide again on the way out.
Location
Store outside the web root, or in a bucket that does not execute. A path built from a user-supplied name is a traversal.
Serving
Serve with an explicit Content-Type and Content-Disposition. An HTML file served as HTML from your origin is stored XSS.
Parsers
Archives, images, XML and spreadsheets are all parsers. Bound size, entries and recursion, and disable external entities.

6 · Secrets, in three places

Look in the repository, the client bundle, and the logs. The second is the one AI-assisted work gets wrong most: a key placed in a client-visible environment variable because the build succeeded and the feature worked. A prefix that exposes a variable to the browser is a publication decision, not a configuration detail.

Rotation matters more than removal. A secret that was committed is compromised even after the commit is rewritten, because the object may already be cloned, cached, or indexed.

7 · Supply chain

Generated code introduces dependencies quickly and confidently, occasionally ones that do not exist yet — which is the entire premise of dependency-confusion squatting. Check that every added package is one you meant, that the lockfile agrees with the manifest, and that install scripts are not doing work you did not ask for.

This is the class where a scanner beats an agent outright. Run the scanner; use the review for whether the dependency should be there.

8 · Agent and MCP integrations

If the application itself calls a model with tools, the security boundary moved. Two questions carry most of the risk: which text reaches the instruction channel, and what a tool is permitted to do once the model names it.

  • Retrieved documents, ticket bodies and web pages are untrusted input. Concatenating them into the prompt makes them instructions.
  • A capability token that binds a run and a tool but not the arguments is not an authorization decision.
  • Read-only and privileged tools should not share one dispatch table with one permission check.

Depth: MCP security audit.

Then prove the fix

The step that gets skipped is the one that makes the rest durable. A fix asserted is not a fix demonstrated. Write the assertion so it fails against the vulnerable control and passes after the change:

Regression test
# The assertion has to fail before the fix and pass after it.
# If it passes against the vulnerable control, it proves nothing.

def test_export_is_tenant_scoped(client, tenant_a, tenant_b, report_of_b):
    client.login(tenant_a)
    response = client.get(f"/reports/{report_of_b.id}/export")
    assert response.status_code == 404

If it passes before the fix, it is testing something else. More on this pattern: security regression testing.

What this page does not claim

To run this order with an agent rather than by hand, install the AppSec agent:

Install
npx skills@latest add omarmohelal/SecHelix --skill sechelix

Apache-2.0, source at github.com/omarmohelal/SecHelix.