
Your AI-built app works. That's not the same as safe.
By now you've seen enough weekend launches. Someone ships a full product in two days, posts the screen recording, and the comments fill up. Nothing wrong with that. AI is a productivity tool, and it should be pushed as far as it goes.
The gap isn't in building. It's in assessing what got built.
An AI-built app looks finished. The login works, the dashboard loads, the charts have real numbers in them. I don't blame anyone for the "wow" moment; I've had it myself. But looking finished and being safe to put in front of paying customers are two different tests, and only one of them is easy to run.
Veracode ran the second test at scale. They gave more than 100 large language models 80 coding tasks across Java, JavaScript, Python and C#, where each task could be completed in a secure or an insecure way. The models took the insecure route in 45% of cases. Their follow-up published in spring 2026 is the part that stayed with me: since 2023, syntax pass rates climbed from roughly 50% to above 95%, while security pass rates stayed flat between 45% and 55%. Models got much better at writing code that runs. They didn't get better at writing code that holds.
The experiment
A couple of weeks ago I gave an AI agent this prompt:
"I want to build a SaaS for managing invoices, including sending them over email. The app should have login, sign up, ability to manage invoices and follow ups for unpaid ones. It should also show the analytics of paid vs unpaid invoices and anything you feel is best to run a small business."
A few iterations later, I had a working SaaS. I could sign up, log in, create an invoice, see the split of paid against unpaid. It felt good.
Then I put the engineer hat back on and audited it with Claude Code. Not with "check this for security issues", which gets you a tidy generic list. I gave it named checks and told it to report against each one. That difference matters more than which tool you use.
Three of the findings are worth writing up.
1. No rate limiting on login
Your login endpoint is open to the internet by definition. That's the job. Which means it's also open to someone pointing a script at it.
The textbook picture of a brute force attack is a program guessing passwords one at a time. The version you'll actually meet is credential stuffing: an attacker takes email and password pairs from somebody else's breach and replays them against your login form, because people reuse passwords. Either way the requirement is the same. The attacker needs a lot of attempts, fast.
With no limit, they get them. And it isn't only an account you stand to lose. Every attempt costs you a database read, a password hash comparison, often an email or an SMS. Someone can run your bill up without ever getting in.
This has a name in the vulnerability catalogue: CWE-307, Improper Restriction of Excessive Authentication Attempts. NIST covers it in SP 800-63B revision 4, which became the current version on 1 August 2025. Verifiers must implement a rate limiting mechanism, and consecutive failed attempts on a single account are capped at no more than 100. That 100 is an upper bound, not a target. You're free to be stricter.
One correction to what I assumed for a long time. Locking the account for 15 to 30 minutes isn't the whole answer, and on its own it hands the attacker a second weapon. If I know your email address, I can fail five logins on purpose and lock you out of your own product. NIST names this risk and offers alternatives: a delay that grows with each failure, 30 seconds building up to an hour, a CAPTCHA before the attempt is accepted, and treating requests from a device or IP the user has authenticated from before differently.
So limit per account and per IP, escalate the delay, and keep the error message identical whether the email exists or not.
2. Anyone with the URL can read the invoice
Say your app puts an invoice at https://example.com/invoice/2131023. That invoice should be visible to the organisation that issued it and the client who received it. Nobody else.
Now change the number. 2131022. 2131024. If the server hands those over, you don't have a permissions bug in one place, you have no permissions layer at all. Every record in the table is one increment away.
The formal name is broken access control, and this particular shape is an insecure direct object reference, CWE-639. OWASP put broken access control at number one in its 2021 Top 10. In the 2025 edition, presented in November 2025 and finalised in early 2026, it's still number one, and OWASP reports finding it in some form in every application in its contributed dataset. Four years of industry attention and the ranking didn't move.
Two things get confused here.
Switching from sequential integers to UUIDs stops someone guessing the next ID. It stops nothing else. A UUID is harder to guess, it isn't a permission. Once that URL is forwarded, screenshotted or written to a log, the record is readable again.
Hiding the button in the frontend isn't access control either. Anyone can open the network tab and call the endpoint directly. The check has to run on the server, on every request, and it has to answer one question: does this user own this record?
AI agents fail this in a specific way. They build a convincing frontend, admin buttons hidden from normal users, premium features greyed out, and leave the server with no enforcement behind any of it. The app looks locked, but it isn't.
3. Secrets sitting in the code
You want to take payments, so you connect Stripe, so you hand the agent an API key. Fine, where does it end up?
The rule everyone knows is to put it in .env, add .env to .gitignore, never commit it. That's correct, and it's incomplete.
Git remembers, and deleting a key from the current version of a file doesn't remove it from the commit history. If it was ever pushed to a public repository, treat it as burned and rotate it; don't just delete the line. GitGuardian's 2026 report counted 28.65 million new hardcoded secrets added to public GitHub during 2025, a 34% rise on the year before, and found that 64% of the secrets it confirmed valid back in 2022 were still valid in 2026. People discover the leak and never revoke the key.
The browser is public. Anything bundled into your frontend ships to the user's machine and can be read from the network tab, whatever file it started life in. In Next.js the NEXT_PUBLIC_ prefix means precisely that. Secret keys belong on the server, behind an endpoint you control. This one is CWE-798, use of hardcoded credentials.
What I actually take from this
None of this is theoretical. Last year a founder posted that their SaaS had been built with Cursor and zero hand-written code; two days later they were posting that their API keys were maxed out and people were bypassing the subscription. Nothing exotic went wrong there, which is the whole point.
I'm not making an argument against building with AI. I use AI agents and tools every week, and I'm not going back.
The argument is narrower than that. The agent builds what you describe. Leave the security posture undescribed, and you get whatever was statistically likely in the training data, which includes a great deal of tutorial code and forum answers that were never written for production. The decisions still get made. They get made by default, by nobody in particular, and you find out which ones when somebody else does.
So before your AI-built app takes a real payment or holds a real customer's data, put the engineer hat on for an afternoon and go through it on purpose.
These are 3 of the 10 checks I run. The full list is in a PDF here: https://lumenharbor.co.uk/ai-readiness-checklist