Skip to content

Dappasol / Guides

By , Founder · Updated September 2026

AI-Generated Code Security Checklist: 25 Things to Check Before You Ship (2026)

Before you ship AI-generated code, verify five risk areas by hand: authentication and authorization, secrets and API keys, input validation, dependencies and hallucinated packages, and error handling that leaks data. Below are 25 concrete checks, each with the exact command to run it. No scanner, no paid tool, no CI pipeline. One afternoon.

The AI wrote it in four seconds. Nobody read it. That is the whole problem, and it is not going away, because the code usually works. It runs, the demo is clean, the feature ships. What it does not do is fail safely when someone hostile shows up.

This is the per-commit checklist. Twenty-five checks you run on the code itself, by hand, before it merges or ships. Every one has a command or a click attached, because a checklist you cannot actually execute is just a list of anxieties. None of it needs a scanner licence, a CI/CD budget, or a security team. You need a terminal, a browser, and an afternoon.

If you want the wider version, the whole-app pre-launch audit that covers payments, backups and infrastructure readiness, that is the 30-point production-readiness audit. Use that one before you launch. Use this one every time you accept a block of AI-generated code.

Quick answer: is your AI-generated code safe to ship

Probably not yet, and the research is not subtle about it. Veracode's 2025 GenAI Code Security Report tested more than 100 large language models across Java, JavaScript, Python and C#, and found 45% of AI-generated code samples introduced an OWASP Top 10 vulnerability. Cross-site scripting was the worst of it: the models failed to defend against it in 86% of relevant samples. Java failed 72% of the time, C# 45%, JavaScript 43%, Python 38%.

The part that should change how you work: performance was flat regardless of model size or training sophistication. So "I'll just use the better model" is not a security strategy. Waiting it out is not either.

The reason is boring rather than sinister. A model is rewarded for producing code that satisfies the request. "Log the user in" gets you a login. It does not get you session invalidation, rate limiting, or a server-side role check, because you did not ask and the demo works without them. Security is the part of the job that is invisible when everything goes right, which is exactly the part a next-token predictor leaves out.

So you add it back. Here is where.

Why AI-generated code needs a different review than human-written code

Reviewing AI output is not the same job as reviewing a colleague's pull request, and treating it like one is why things slip through.

A human writes insecure code in a pattern you can predict from knowing them. They are rushing, or they misunderstood the auth model, or they are new. The mistakes cluster and they have a reason. A model has no such consistency. It can write a textbook-correct parameterized query on line 40 and concatenate a raw string into SQL on line 96 of the same file, because those two lines came from different regions of the training data. Local correctness tells you nothing about the line below it.

Three things follow from that, and they shape the whole checklist.

Confidence is not a signal. Human code carries tells when the author was unsure: a comment, a TODO, an awkward name. AI output is uniformly fluent. The dangerous line and the safe line are written with identical poise, so your instinct for "this bit looks shaky" does not fire. You have to check mechanically instead of intuitively.

It invents things that do not exist. No human colleague imports a package that was never published. Models do it constantly, which is a failure mode with no equivalent in normal review and needs its own explicit check. More on that in the dependency section.

Volume defeats attention. The reason people adopt these tools is throughput, and throughput is precisely what breaks careful review. Nobody reads 800 accepted lines a day with the same eyes they brought to the first fifty. This is why the checks below are commands rather than instructions to "review carefully." A grep does not get tired at 6pm.

Authentication and authorization checks

Start here. Access control is the category that leaks other people's data, and it is the one AI tools skip most reliably, because a broken authorization check looks exactly like a working one until someone tries.

1. Every protected endpoint checks auth on the server

The common AI pattern is a guard in the component: it hides the link, redirects in a useEffect, and leaves the API route wide open. The UI looks locked. The data is not.

curl -i https://yourapp.com/api/orders

Run that logged out, with no cookie or token, against every endpoint that returns something private. Pass: 401 or 403. Fail: a 200 with real data, which means your only lock was a hidden button.

2. A logged-in user cannot read another user's records

This is broken object-level authorization, and it is the single most damaging gap we find. The endpoint checks that you are logged in, then hands over whatever ID you asked for without checking it is yours.

Log in as your own test user, grab a record ID, then request a different one:

curl -i -H "Cookie: $YOUR_SESSION" https://yourapp.com/api/orders/999

Pass: 403 or 404 for anything you do not own. Fail: someone else's order. Repeat on the update and delete routes, not just the read.

This is not theoretical. A Lovable-built app leaked 18,697 user records, 14,928 of them unique email addresses, including 870 users whose full personal information was exposed, as reported by The Register in February 2026. The cause was a malformed authentication function that inverted its own logic: it blocked authenticated users and allowed unauthenticated ones. The code ran. It just ran backwards, and nothing about reading it casually would tell you.

3. Role checks happen server-side, not against a hardcoded value

Look for admin checks that compare an email string, or read a role from a client-supplied payload rather than from the session on the server.

grep -rnE "isAdmin|role ===|user\.email ===|'admin'" src/ app/ server/

Pass: the role is loaded from your database using the server-verified session identity. Fail: if (user.email === "me@mycompany.com"), or any role that arrived in the request body, which the client can simply set.

4. Logging out actually invalidates the session

Plenty of AI-generated logout handlers clear local storage and redirect. The token stays valid until it expires, so anyone who captured it keeps your account.

Copy your session cookie or bearer token, log out in the browser, then replay the old token with curl.

curl -i -H "Cookie: $OLD_SESSION" https://yourapp.com/api/me

Pass: 401. Fail: your profile, cheerfully returned to a session you thought you killed.

5. Password reset tokens cannot be guessed or reused

Trigger a reset on a test account and read the token in the link. Pass: long, random, single-use, and expiring in an hour or less. Fail: anything derived from data an attacker knows. If the token is the user ID, a sequential number, a timestamp, or a base64 blob that decodes to the email address, that is account takeover for anyone who can guess an email. Then click the same link twice: it should work once.

Secrets, API keys, and credential checks

Secrets in the client bundle are the fastest way to lose money rather than data, because a leaked payment or cloud key gets used by strangers within hours of being findable.

6. No secrets survive into the built bundle

Check the build output, not the source. The source is allowed to reference a secret; the bundle is not allowed to contain one.

npm run build
grep -rEi "sk_live|sk_test|service_role|-----BEGIN|AIza|xox[baprs]-" dist/ build/ .next/static/

Pass: zero hits. Then confirm in the browser: open DevTools, Sources, search all files for secret and key. Anything a user can read in DevTools is public, whatever your intent was.

7. Nothing sensitive is behind a public env prefix

Any variable prefixed NEXT_PUBLIC_, VITE_, REACT_APP_ or PUBLIC_ is compiled into JavaScript and shipped to every visitor. That is the documented behaviour, not a bug, which is exactly why it catches people.

grep -rn "NEXT_PUBLIC_\|VITE_\|REACT_APP_\|PUBLIC_" .env* src/

Pass: only genuinely public values, like an analytics ID or a Supabase anon key that is protected by row-level security. Fail: a Stripe secret key, a service-role key, an OpenAI key, or a database URL.

8. Git history is clean, not just the current files

Deleting a key from a file does not delete it from the repository. It sits in history forever, and history is the first place anyone looks.

gitleaks detect --source . -v

Gitleaks is free and installs in one command. If you would rather not install anything, git log -p -S"sk_live" --all covers a single pattern. Pass: no findings across the full history.

9. Every exposed key has been rotated, not just moved

This is the check people skip, and skipping it makes checks 6 through 8 pointless. If a key was ever in a client bundle, a public repository, a screenshot, or a chat log, it is compromised. Moving it to an environment variable changes nothing about the copy someone already has.

For every key the earlier checks surfaced, open the provider dashboard and confirm the creation date is after the exposure. Pass: new key, old key revoked. Fail: "I moved it to .env", which is not rotation.

Input validation and injection checks

This is where the Veracode numbers bite hardest. An 86% failure rate on cross-site scripting means you should assume the model got it wrong and go looking for proof it did not.

10. Validation exists on the server, not only in the form

AI-generated forms usually validate beautifully in React and accept anything at the API. The browser is not a security boundary. Anyone can skip it.

curl -X POST https://yourapp.com/api/orders \
  -H "Content-Type: application/json" \
  -d '{"quantity":-5,"price":0,"email":"not-an-email"}'

Pass: 400 with a validation error. Fail: 201, and you now own a negative-quantity order. Try each endpoint with a missing field, a wrong type, a negative number, and a string where you expect a number.

11. Every SQL query is parameterized

grep -rnE "(query|execute|raw)\(.*(\+|\$\{|%s|f\")" src/ server/ api/

That finds string interpolation reaching a query call, which is the shape of an injection. Pass: every database call passes values as separate parameters or goes through an ORM's binding. Fail: `SELECT * FROM users WHERE id = ${id}`. Watch search, filter and sort parameters especially, where the column name is dynamic so the model builds the string by hand.

12. User content is escaped before it reaches the page

grep -rn "dangerouslySetInnerHTML\|v-html\|innerHTML\|\.html(" src/

Every hit is a place raw HTML gets injected into the DOM. Then test it live: put <img src=x onerror=alert(1)> into every field that gets displayed back, including names, bios, comments, and anything that lands in an admin dashboard. Pass: the text renders as literal characters. Fail: an alert box, which means an attacker can run script in your users' sessions.

13. File uploads are restricted and served inertly

Try uploading an .html file, and an .svg with a script tag inside it. Then open the URL the app gives you back.

Pass: rejected on type, or stored somewhere it cannot execute and served with Content-Disposition: attachment or a non-rendering content type. Fail: your script runs on your own domain. Also confirm there is a size limit, and that the filename cannot contain ../.

Dependency and hallucinated-package checks

This category has no equivalent in human code review, and it is the one most people have never heard of.

Models invent packages. In a study of 16 code-generating LLMs across 576,000 generated samples, researchers found 5.2% of packages recommended by commercial models did not exist, rising to 21.7% for open-source models, producing 205,474 unique hallucinated package names. The supply-chain problem writes itself: attackers watch for names models invent repeatedly, register them, and wait for someone to run install.

14. Every dependency is a real package, and the one you meant

For any package name you do not personally recognise in package.json:

npm view <package-name> time.created maintainers versions

Pass: created years ago, real maintainers, a long version history, meaningful download counts. Fail: published three weeks ago, version 1.0.0, one anonymous maintainer, near-zero downloads, and a name one character away from something famous.

15. No known-vulnerable dependencies

npm audit --omit=dev
pip-audit          # Python
cargo audit        # Rust

Pass: zero critical or high findings in production dependencies. Dev-only findings are lower priority but read them. These tools are free and ship with the ecosystem, so there is no reason not to run this on every commit that changes a lockfile.

16. The lockfile is committed and installs are reproducible

git ls-files | grep -E "package-lock.json|pnpm-lock.yaml|yarn.lock|poetry.lock"
npm ci

Pass: a lockfile is tracked and npm ci succeeds. Fail: no lockfile, which means the version you audited is not necessarily the version that deploys.

17. Nothing unexplained got added along the way

npx depcheck

AI tools add dependencies casually, and abandoned ones accumulate. Pass: you can name why each package is there. Anything unused that touches the network, the filesystem, or the shell comes out now.

Error handling and data-exposure checks

Leaks here are quieter than the others. Nothing breaks, so nothing tells you.

18. Errors do not return stack traces

curl -i "https://yourapp.com/api/orders?id=';--"

Pass: a generic message and an error reference ID. Fail: a stack trace, a file path, an ORM error naming your tables and columns, or a database connection string. A stack trace hands an attacker your schema and your framework versions for free. Confirm NODE_ENV is genuinely production, since most frameworks gate verbose errors on it.

19. Logs do not contain secrets or personal data

grep -rn "console.log(req\|console.log(process.env\|print(request\|log(headers" src/

Logging the whole request object captures auth headers, cookies and full request bodies, which means passwords in plain text sitting in a log aggregator half your vendors can read. Pass: logs contain identifiers, not payloads. Then read a hundred lines of real production log output and confirm it.

20. API responses return only the fields the client needs

Open DevTools, Network tab, and click through your own app reading each JSON response.

grep -rn "select('\*')\|SELECT \*\|findMany()" src/

Pass: responses contain exactly what the UI renders. Fail: password_hash, stripe_customer_id, internal flags, other users' email addresses, or a user object with thirty fields where the page shows a name. The UI hiding a field is not the same as the API not sending it.

21. Debug endpoints and source maps are not public

for p in /.env /api/debug /graphql /assets/index.js.map /.git/config; do
  echo "$p $(curl -s -o /dev/null -w '%{http_code}' https://yourapp.com$p)"
done

Pass: 404 on all of them. Source maps rebuild your entire unminified source, comments included.

Infrastructure and deployment checks

22. HTTPS is enforced and security headers are set

curl -sI https://yourapp.com | grep -iE "strict-transport|content-security|x-frame|x-content-type"

Pass: HSTS present, a content security policy, and clickjacking protection via X-Frame-Options or a frame-ancestors directive. SecurityHeaders.com grades this in one click if you prefer a browser. Confirm plain HTTP redirects rather than serving.

23. CORS is not open to the world

curl -sI -H "Origin: https://evil.example" https://yourapp.com/api/me | grep -i access-control

Pass: your own origins only. Fail: the response reflects https://evil.example back, or returns Access-Control-Allow-Origin: * alongside Allow-Credentials: true. "Allow everything" is the standard AI fix for a CORS error during development, and it survives into production a lot.

24. Rate limiting protects login, payment and expensive endpoints

for i in $(seq 1 30); do
  curl -s -o /dev/null -w "%{http_code} " -X POST https://yourapp.com/api/login \
    -H "Content-Type: application/json" -d '{"email":"a@b.com","password":"wrong"}'
done

Pass: 429 shows up well before thirty. Fail: thirty 401s, which is an open invitation to credential stuffing. Run the same loop against any endpoint that calls a paid API, because without a cap one script turns your model bill into a five-figure number overnight.

25. Production, staging and local are genuinely separate

Compare the database host and credentials in each environment. Pass: different hosts, different credentials, and no production connection string anywhere in your local .env or your teammate's laptop. Fail: one database with a test_ prefix convention, which works right up until a migration written by an AI runs against the wrong one.

How to run this checklist without buying a scanner

Everything above uses tools that cost nothing. Here is the whole kit and roughly what each stage takes.

Risk areaChecksFree tool that helpsTime to run
Authentication and authorization1 to 5curl, two browser profiles45 min
Secrets and API keys6 to 9gitleaks, grep, DevTools Sources30 min
Input validation and injection10 to 13curl, grep, Semgrep free rules45 min
Dependencies and hallucinated packages14 to 17npm audit, pip-audit, npm view, depcheck20 min
Error handling and data exposure18 to 21DevTools Network, curl, grep30 min
Infrastructure and deployment22 to 25curl, SecurityHeaders.com20 min

That is a little over three hours for a full pass. The first run is the slow one. After that, most of it collapses into a handful of greps you can paste in before a merge.

Two installs are worth the five minutes: Gitleaks for credentials across your whole git history, and Semgrep, whose free rule set catches injection and hardcoded-secret patterns statically. Neither needs an account.

What they will not do is understand your application. A scanner reads a working login and sees a working login. It cannot tell that any authenticated user can read every other user's rows, because both the safe and the unsafe version are syntactically fine. Checks 2, 3 and 20 are the ones that catch that, and all three are things you do by hand. Automate what pattern-matches, and reserve your own attention for authorization and business logic, which is where the expensive failures live.

When to bring in a human security review instead

This checklist has a ceiling, and pretending otherwise would waste your time. Do it yourself when you are shipping features, iterating pre-launch, or working on something where the worst case is embarrassment. Get another set of eyes when the worst case is somebody else's money or personal data.

Specifically, stop and get help if any of these are true. You handle payments, health data, or anything under GDPR or HIPAA. You had an incident and do not know its full extent. You inherited the codebase. Money is moving: an acquisition, an investment round, or an enterprise security questionnaire. Or you ran the checks above, found problems in most categories, and cannot tell which three actually matter.

That last one is the real dividing line. Finding issues is the easy half. Ranking them, and knowing which are launch-blocking versus which can wait a quarter, is judgment, and judgment is what you are buying when you pay someone.

Our version is the Week-1 Build Audit: $500 flat, one week, and you get a written, severity-ranked blocker list with a fix estimate against every item, marked fix-now or can-wait. Every blocker found or it costs you nothing, and the $500 is credited 100% against any fix or build afterwards. If the report says you were mostly fine, that is a useful thing to know for $500 too.

Before any of that, run the free checks. Turning up nothing is a legitimate outcome and it costs you an afternoon.

Common launch-blocker patterns we see in AI-built apps

Across the AI-built apps we audit, the same handful of failures repeat. Not similar ones. The same ones, in roughly this order of frequency.

Row-level security was never switched on. The schema is fine, the app works, and every user can read every other user's rows by changing a number in a URL. This is check 2, it is the Lovable failure mode, and it is both the most common gap and the most damaging.

A secret got a public prefix. Someone hit an environment variable that came back undefined in the browser, the AI suggested adding NEXT_PUBLIC_, it worked, and a service-role key now ships to every visitor. The fix takes a minute. The rotation nobody does afterwards is check 9.

CORS was set to * to make a development error go away. Never revisited. Now any site can make credentialed requests to your API from a victim's browser.

Authorization exists on read but not on write. Someone carefully secured the GET route and left PATCH and DELETE open, because the checklist in their head said "protect the endpoint" and they tested the one they were looking at.

Zero rate limiting anywhere. Login is brute-forceable and the endpoint that calls a paid model API has no cap at all. This one usually gets discovered via a bill.

Errors return everything. NODE_ENV was never set in production, so the framework is still in development mode and every 500 returns a full stack trace with file paths and table names.

If you built on a specific tool, the platform-shaped version of this is worth reading: fix my Lovable app covers what breaks in Lovable builds, and is Cursor safe looks at the IDE-based tools. For the research behind why this keeps happening, is AI-generated code safe has the 2026 data. And when you are done with the code and ready to think about the whole application, go back to the 30-point production-readiness audit, which covers payments, backups and monitoring that this per-commit list deliberately leaves out.

None of this means stop using AI to write code. It means read what it hands you. The tools are genuinely good at producing working software and genuinely indifferent to whether that software is safe, and the gap between those two things is your job now.

FAQ

How do I check if AI-generated code is secure?

Work through five risk areas in order: authentication and authorization, secrets and API keys, input validation, dependencies, and error handling. The highest-value single test is calling a protected API endpoint with no session (it should return 401, not data) and then calling it with your own session but someone else's record ID (it should return 403 or 404). Broken object-level authorization is the most common and most damaging gap in AI-built apps.

Do I need a security scanner to review AI-generated code?

No. Every check in this guide runs with curl, grep, browser DevTools, and free tools that ship with your language: npm audit, pip-audit, gitleaks and Semgrep's free rules. A full pass takes about three hours. Scanners help with pattern-level bugs, but they cannot spot a broken authorization check, because the safe and unsafe versions are syntactically identical.

What is the most common security problem in AI-generated code?

Missing access control. The endpoint checks that you are logged in, then returns whatever record ID you asked for without checking you own it. One Lovable-built app exposed 18,697 user records this way, 14,928 of them unique email addresses, after an authentication function inverted its own logic and blocked authenticated users while allowing unauthenticated ones.

Can AI write secure code if I just ask it to?

It helps, but it is not a substitute for checking. Veracode tested more than 100 models and found 45% of AI-generated code introduced an OWASP Top 10 vulnerability, with performance flat regardless of model size or sophistication. Asking for secure code raises the odds; it does not make verification optional, and newer models did not test safer than older ones.

How long does it take to review AI-generated code for security?

About three hours for a full 25-check pass the first time: 45 minutes on authentication, 45 on input validation, 30 each on secrets and error handling, 20 each on dependencies and infrastructure. After the first run most of it collapses into a handful of greps you paste in before a merge, which takes a few minutes per commit.

When should I pay someone to audit AI-generated code?

When the worst case is someone else's money or personal data: payments, health data, anything under GDPR or HIPAA, a codebase you inherited, a suspected incident, or diligence for an acquisition or investment. Also when you have found issues in most categories and cannot tell which three are actually launch-blocking. DappaSol's Week-1 Build Audit is $500 flat for a written, severity-ranked blocker list, credited in full against any fix.

Just ran the 25 checks and something came back wrong? Send us the repo or the URL. We will tell you the worst thing we find, in plain English, for nothing. If it is a one-line fix we will tell you how to do it yourself. If it is not, at least you will know what you are dealing with before you spend anything.