Skip to content

12 security holes AI leaves in your app, and how to close them

A copy-ready prompt to close each of the twelve security holes AI leaves in most apps, and a browser check under each one to prove it actually shut.

19 min read

Your app works. That is not the same as safe.

AI writes code that runs, and it reliably leaves the same handful of doors open while it does. Nobody breaks into an app like this. They walk through a door the tool left open, because the app was built for the path where everything goes right and never for the person who edits the address bar.

This is the full set: twelve holes AI leaves in almost every app it builds, a copy-ready prompt to close each one, and, under every prompt, a check you run yourself in a browser with no code to read. That last part is the point. The same tool that left the door open is the one that will tell you it is shut, so the checks are the only part of this guide that does not take the AI’s word for it.

Nothing here is tied to one stack or one tool. The prompts are worded to work in any chat where you build, against any database.

The twelve doors

Tick a box only after the check on that door passes, not when the AI says it is done.

Keys and the cabinet

  • ☐ 1. API keys sitting in the frontend
  • ☐ 2. Secrets left in Git history
  • ☐ 3. A database anyone can read with the URL

Doors that never ask who you are

  • ☐ 4. Access checked only in the interface
  • ☐ 5. Change the ID in the URL, see someone else’s data
  • ☐ 6. The whole request body is trusted
  • ☐ 7. No rate limit anywhere

What comes in, what goes out

  • ☐ 8. Input checked in the browser only
  • ☐ 9. Queries built by gluing strings together
  • ☐ 10. User content shown raw
  • ☐ 11. Uploads accept anything
  • ☐ 12. The API hands back the whole row

Five minutes before you run anything

Do this on your live app, in a browser, right now. No code, each check under a minute, and each one maps to a door. Most people find three or four open, and that is not a verdict on you. It is what the tool shipped.

Do this If this happens, the door is open
Open your live site, view the page source, and search it for key, secret, token, sk_, sk- A real-looking key is sitting in the page (door 1)
Sign out. Open your database’s public data URL in a fresh tab You can see rows while logged out (door 3)
Sign out. Type an admin or dashboard URL straight into the address bar The page renders at all, even half-broken (door 4)
Sign in as yourself. Find a URL with your ID in it and change the number by one You see anything that is not yours (door 5)
Sign out. Type a wrong password ten times, fast Ten tries, no block and no slowdown (door 7)
On a profile or list page, open the browser’s network panel, reload, click the data request, and read the response Fields the screen never shows: phone, address, a password hash, card digits (door 12)

The six you cannot test from a browser (2, 6, 8, 9, 10 and 11) are found by the prompts below, which is most of what this guide is for.

The honest timing: the twelve prompts take about twenty minutes to run. Reading the changes, testing that nothing broke, and rotating anything that leaked takes the rest of an evening. Budget the evening, not the twenty minutes.

The order to run them, and three warnings

Run them in order, 1 through 12. It is the order an attacker meets your app, and it saves wasted work: there is no point tightening a query on a database that anyone can already read. If you only have one evening, do 1 through 5 and stop. The first three are your keys and your whole database, and doors 4 and 5 are the two a non-technical builder can never see and the two AI gets wrong most often.

Before you start, three things that are true and inconvenient.

Two of these are not done when the AI says done. Doors 1 and 2 both end with a key that was public. Moving it and purging it changes nothing for anyone who already copied it. You have to open the provider’s own dashboard and rotate the key by hand, then put the new one in a server-side environment variable. Skip the rotation and you did twenty minutes of work with the door still open. Rotate even if the bill looks normal.

Three of these can lock you out of your own app. Doors 3, 6 and 11 all work by refusing things that used to be allowed. Turning on database access rules with no policy means nothing loads. Restricting fields means legitimate updates start silently doing nothing. Capping uploads can break files already stored. After each of those three, sign in as a normal user and do the three things that user actually does. Before all three, commit, so you can get back.

One of these you cannot verify from outside. Door 9 is the only one where the check is “I looked and there were none”, and you cannot prove that from a browser. Its check is a list you read, not a thing you click, which is exactly why it is the one most often skipped.

Group 1: keys and the cabinet

1. API keys sitting in the frontend

What someone walks out with: your payment and AI keys, straight from the page source, in seconds. They call the paid service and the bill comes to your card.

Prompt
Search this entire project for any API key, secret, token, password or
connection string that sits in client-side code, a public config, an
environment variable exposed to the browser, or anything shipped to the
frontend. List every one with its file and line, and say which service it
belongs to. Then move each to a server-side environment variable and show
me the diff before applying anything. Do not commit, and do not touch the
database yet.

Check: load your live site, view the page source, and search it for sk_, sk-, key and secret. Then open the network panel while using the feature. The request should go to your own domain, never straight to the payment or AI provider. Then rotate every key that was exposed, in that provider’s dashboard. That step is yours, and the AI cannot do it. For the bill side of this, see AI cost control.

2. Secrets left in Git history

What someone walks out with: the key you deleted this morning, out of last week’s commit. Automated scanners read public repositories for exactly this, and quickly.

Prompt
Scan the full Git history of this repo, every branch and every commit, for
API keys, tokens, passwords and environment files that were ever committed,
including ones deleted later. List each with its commit hash and file. Then
tell me exactly how to purge them from history and what will break for
anyone who has already cloned the repo. Do not run the purge yet.

Check: search your own history for one key you know used to be there. If it still comes back, the purge did not take. Then rotate every key that ever appeared in history, whether or not the repo is public. A purged history and a live key is not a fix.

3. A database anyone can read with the URL

What someone walks out with: the whole table. Not a leak, a download, using the same public key your app already ships to every browser, with no login at all.

Prompt
List every table in this database and tell me, for each, whether row-level
access control is on and what policies exist. For every table that holds
data belonging to a user, write the policies so a user can read and write
only their own rows, and so anonymous access is denied by default. Show me
the policies as a plain-English list I can read before you apply anything.

Check: sign out completely and open your database’s public data URL in a private window. You should get an empty result or a permission error, never rows. Then sign back in as a normal user and use the app for two minutes. This is the prompt most likely to break everything, and you want to find that out now, not from a user.

Group 2: doors that never ask who you are

These four are the ones AI gets wrong most often, and the ones you cannot see from your own screen. Everything looks correct while you are logged in as yourself. That is the problem.

4. Access checked only in the interface

What someone walks out with: the admin page. The button was hidden. The address bar was not.

Prompt
List every page and every API route in this app and tell me where the
logged-in user is checked. For each one, say whether the check happens on
the server or only in the interface. Anywhere it is only in the interface,
add a server-side check that runs before any data is read or written.
Hiding a button is not a check. Show me the list first, before you change
anything.

Check: sign out. Paste your admin or dashboard URL straight into the address bar. You should be sent to the login page, not shown an empty version and not shown a broken one. An empty page means the route ran and found nothing, so the door is still open.

5. Change the ID in the URL, see someone else’s data

What someone walks out with: another user’s orders and phone number, by editing a 1 into a 2. The query asked “fetch this ID”. It never asked “is this theirs?”. This is the single most common serious hole in apps built quickly, and OWASP ranks it first in its Top 10.

Prompt
Find every database query in this app that fetches or updates a record by
an ID that came from a URL, a form or a request. For each one, show me
whether it also filters by the logged-in user's ID. Add that filter
anywhere it is missing, and return a not-found rather than an error when
the record is not theirs. Give me the list of every query you found and
what you changed in each.

Check: you will need two accounts, so make a second one. Sign in as account A, find a URL with an ID in it, and put account B’s ID in its place. You should get a not-found. Do the same on a delete or an edit URL, not only a view. If reading someone else’s code is the part you are unsure about, the code review guide covers this door and the two around it.

6. The whole request body is trusted

What someone walks out with: an admin badge. The form had two boxes. The request can carry a third, and a field like role: admin is accepted because nobody said it could not be.

Prompt
For every endpoint that accepts data from a user, list exactly which fields
it currently lets through into the database. Then restrict each one to an
explicit allowlist of the fields that user is permitted to set, and drop
everything else. No user may set their own role, permissions, price,
credits, status or IDs. Show me the allowlist per endpoint as a table
before you apply it.

Check: read the table it gives you and look for anything that decides what a user is allowed to do or what they pay. Then sign in as a normal user and edit your profile, place an order and change a setting, all three of which should still work. Silently dropped fields are the usual way this fix breaks an app.

7. No rate limit anywhere

What someone walks out with: a hundred thousand password guesses overnight, or a runaway AI bill by morning. No break-in required, only patience it does not need to have.

Prompt
Add rate limiting to login, signup, password reset, one-time codes, contact
forms and every route that calls a paid AI, SMS or email service. Tell me
the limit you chose for each and what a blocked user sees. Then tell me
where I set a hard monthly spending cap on each paid service, in that
provider's own dashboard.

Check: sign out and type a wrong password ten times, fast. You should be blocked or slowed, with a message that makes sense. Then set the monthly cap yourself, in each provider’s dashboard. A limit in your code does not stop a bill. A cap does.

Group 3: what comes in, what goes out

8. Input checked in the browser only

What someone walks out with: anything at all. “Maximum 20 characters” is a sticker on the outside of the form. Skip the form, send the request straight, and nobody inside is checking.

Prompt
For every endpoint, validate the incoming data on the server against an
explicit schema: type, required, length, format and allowed values. Reject
anything that does not match, and anything with extra fields, with a clear
error. Do not rely on any check that exists only in the browser. List every
endpoint and the schema you gave it.

Check: find a field with a length or format limit. In the browser’s element inspector, delete the maxlength or required attribute on that input and submit. The server should still reject it. If it saves, the only lock was the one you just removed with two clicks.

9. Queries built by gluing strings together

What someone walks out with: the whole table, out of one search box, because what they typed was glued onto the end of your query and the database read all of it as instructions.

Prompt
Find every database query in this project that is built by joining or
inserting strings, anywhere user input can reach it: search, filters,
sorting, raw SQL, dynamic column or table names. List each with its file
and line. Then rewrite each as a parameterized query, and for anything that
cannot be parameterized, use a strict allowlist of permitted values. Show
me the list before you change anything.

Check: this is the one you cannot test from a browser. Read the list it gives you, then ask a second, fresh chat: “Find every query in this project built from string concatenation with user input. List them.” If the fresh chat finds any the first missed, the first pass was not complete. Two independent passes is the closest you get to proof here.

10. User content shown raw

What someone walks out with: other people’s sessions. One comment that is not words but a small program, running in the browser of everyone who scrolls past it.

Prompt
Find every place in this app where content written by a user is shown to
other users: comments, names, bios, messages, reviews, file names,
anything. For each, tell me whether it is escaped or rendered as raw HTML.
Escape everything by default. If anything must allow formatting, sanitize
it against a strict allowlist of tags instead. List every location and what
you did to it.

Check: post a comment containing <b>test</b> and a stray <script> tag. It should appear on screen as that exact text, tags visible, not as bold text and not as nothing. Bold text or disappearing tags both mean it was interpreted rather than shown.

11. Uploads accept anything

What someone walks out with: your server. A huge file where a photo was expected stops the app for everyone. A program with a photo’s name sits inside and waits.

Prompt
For every file upload in this app, check the real file type from its
contents, not its extension or the type the browser claims. Cap the file
size and tell me the cap. Rename every uploaded file to a random name.
Store uploads in object storage, outside the app directory, and never serve
them from a path where they could execute. Tell me what happens to files
already uploaded under the old rules.

Check: rename a text file to end in .jpg and upload it. It should be rejected. Try a file bigger than the cap. It should fail cleanly, with a message, not a spinner that never ends. Then confirm your existing images still load. This is the third of the three prompts that can break a working app.

12. The API hands back the whole row

What someone walks out with: the fields the screen was hiding. The page shows a name. The response carries the phone number, the address, the password hash and the last four digits of the card. Hiding is not deleting.

Prompt
For every API endpoint, compare what the screen actually displays with what
the response actually contains. List every field being sent that the
interface never shows. Then change each endpoint to return only the fields
that screen needs, and never password hashes, internal IDs, tokens, full
card data or another user's details. Show me the before and after field
list per endpoint.

Check: open the browser’s network panel, load a profile or a list, click the data request, and read the response. Every field in there is a field a user can read. If it is not on the screen, it should not be in the response.

Two more doors

Two that did not make the twelve. Both real, both a little less common.

Error pages that show the internals. An error page that prints file paths, library versions or a raw database error is handing over a map of your app.

Prompt
Make sure production never shows stack traces, file paths, library versions
or raw database errors to a user. Log the full detail on the server, and
show the user a plain message and a reference code. Tell me how to trigger
an error safely so I can check what it looks like in production.

Check it by visiting a URL that does not exist, and submitting a broken form, on the live site. You should see a plain message. If you can read a file path, it is still open.

CORS set to a wildcard. A wildcard lets a page on any website call your API from a browser and read the response. It does not hand over your users' logged-in sessions, because browsers refuse to send credentials to a wildcard origin, but it is still not the setting you want, and it is never the fix for a blocked request.

Prompt
Show me the CORS configuration for this app. Replace any wildcard origin
with an explicit list of the domains actually allowed to call this API, and
tell me which domains you put on the list and why. Do not allow credentials
with a wildcard origin.

Check it by reading the list. Every domain on it should be one you own or can name. If you cannot say why a domain is there, it should not be.

Make it stick

Closing the twelve once does not keep them closed. The next feature reopens them unless the rules live where the AI reads them. Put this in your tool’s project instruction file, such as CLAUDE.md, AGENTS.md or your editor’s rules file. If you do not have one, the 5-file starter pack explains where it goes.

Prompt
SECURITY RULES FOR THIS PROJECT
- No keys, secrets or tokens in client code, public config or Git.
  Server-side environment variables only.
- Access control on every table with user data. Deny anonymous by default.
- Check the logged-in user on the server for every route. The interface is
  not a lock.
- Every query that reads or writes a record filters by the logged-in
  user's ID.
- Allowlist the fields a user may set. Nobody sets their own role, price
  or status.
- Rate limit login, signup, password reset and every paid AI route.
- Validate every input on the server against a schema. Reject extra fields.
- Parameterize every query. Never build one from strings.
- Escape all user content on output by default.
- Uploads: verify the real type, cap the size, rename, store outside the
  app.
- Return only the fields the screen needs, never the whole record.
- No stack traces in production. No wildcard CORS.

To confirm it is being read, open a new chat and ask: “What security rules are you following in this project? List them from memory before you do anything.” A vague or generic answer means the file is not being picked up, so check its name and that it sits in the project root.

What this is, and what it is not

Twelve prompts are not a security audit. They close the twelve holes AI leaves most often, and they say nothing about your business logic, your payment flow, your backups, your admin accounts or anyone you have given access to. If you handle health data, data about children, or other people’s money at any scale, this is your floor and not your ceiling, and it is worth having a professional review the app.

Three things that are true and inconvenient. A rotated key is not optional. An access policy you never tested as a real user is not a policy. And a check that “probably passed” did not pass.

If something does leak, rotate every key in the provider’s dashboard first, before you diagnose anything. Then read the provider’s usage logs for what was called and when, and your database’s row counts for anything deleted. In that order: the diagnosis can wait ten minutes, the key cannot.

Before you take any of this live, the deploy guide covers the other way a working app falls over, which is the move from your laptop to a server.

The one line to keep, if you forget the rest: your app working and your app being safe are two different facts, and only one of them has been tested.

Common questions

Is an app built with AI secure by default?
No. AI writes code that works, and working is not the same as safe. It reliably leaves a handful of the same holes open, most often a page that checks who you are only in the browser and a database query that fetches a record by its ID without checking who owns it. Nobody breaks into apps like these. They walk through a door that was left open.
What is the most common security hole in AI-built apps?
Broken access control, which is one record reachable by another user who changes the ID in the address bar, and one admin page that loads because the button was hidden but the route was never protected. It is the flaw OWASP ranks first in its Top 10, and it is the one you cannot see while logged in as yourself, because everything looks correct from your own screen.
Can I trust the AI to fix its own security holes?
Only if you check its work. The same tool that left the door open is the one reporting the door is shut, so every fix here ends with a check you run yourself in a browser, with no code to read. Ask for the list of changes before they are applied, test as a normal user afterwards, and rotate any key that was exposed yourself, because the AI cannot do that part.
Do these twelve prompts replace a security audit?
No. They close the holes AI leaves most often, and they say nothing about your payment flow, your business logic, your backups or the people you have given access to. If you handle health data, data about children, or other people’s money at any scale, treat this as your floor and have a professional review the app.