How to secure an AI-generated app before real users touch it.

The eight security problems that recur in AI-generated apps, from missing ownership checks to leaked secrets — with a fix and a test for each one.

By UmaizPublished 10 min read

Securing an AI-generated app is mostly about eight specific mistakes, not about security in general. Generated code isn’t insecure because the model writes bad code — it’s insecure because security lives in the relationships between pieces, and each piece was written without knowing about the others. The same eight problems turn up in Lovable, Bolt, Cursor and ChatGPT projects alike, and each has a fix you can make in an afternoon.

They’re ordered by how bad the outcome is if you skip them.

1. Ownership checks on every record

The problem. Endpoints verify you’re logged in but not that the record you asked for belongs to you. Change a number in the URL and you get someone else’s data. This is the single most common serious finding in generated backends.

The fix. Compare the record’s owner to the current user on every read and write. Return 404 rather than 403 for records that aren’t yours, so you don’t confirm they exist.

const doc = await db.documents.findById(req.params.id)
if (!doc || doc.ownerId !== req.user.id) {
  return res.status(404).json({ error: 'Not found' })
}

The test. Log in as user A. Take a record ID belonging to user B. Request it. If anything comes back, you have work to do.

2. Secrets out of the client and out of git

The problem. An API key in frontend code is public — it ships to every browser. A key committed to git once stays in history after the file is deleted, and history is what gets cloned.

The fix. Move every secret to environment variables read only on the server. Then check both places it might already have leaked:

# The built frontend
grep -rniE "sk_live|api[_-]?key|secret" dist/ .next/static/ 2>/dev/null

# Git history, including deleted files
git log --all --full-history -p -- .env .env.local | grep -iE "key|secret" | head

If a key was ever committed, rotate it. Removing it from the repo does not un-leak it.

3. Nothing about money trusted from the client

The problem. Price, quantity, discount or plan read from the request body. The browser sent it, so anyone can send anything.

The fix. Look up prices server-side from the product ID. Recompute totals on the server. For payment providers, treat a redirect back to your site as nothing — confirm success from the provider’s API or a signed webhook.

// Never this
const total = req.body.price * req.body.quantity

// Always this
const product = await db.products.findById(req.body.productId)
const total = product.price * clamp(req.body.quantity, 1, 100)

The test. Post to your checkout endpoint directly with price: 0.01. If it goes through, that’s your finding.

4. Server-side validation

The problem. Validation only in the form. Client-side checks are a courtesy for honest users; anyone can post directly to the API.

The fix. Validate every request body on the server with a schema. Reject, don’t coerce.

const schema = z.object({
  email: z.string().email().max(200),
  name: z.string().trim().min(1).max(100),
})
const result = schema.safeParse(req.body)
if (!result.success) return res.status(400).json({ error: 'Invalid input' })

5. Rate limits on anything that costs you

The problem. Login, signup, password reset, uploads and anything that calls a paid API — with no limits. One script turns your LLM bill into a story, or lets someone try ten thousand passwords.

The fix. Per-IP and per-account limits on those routes specifically. Most frameworks have a middleware for it; it’s ten lines.

The test. Hit your login endpoint 200 times in a loop. If all 200 get a real response, there’s no limit.

6. Auth tokens that expire and can’t be replayed

The problem. Password reset links that never expire, or can be used twice. Session tokens in localStorage where any injected script can read them.

The fix. Reset tokens: single-use, expire in under an hour, invalidated on password change. Session cookies: httpOnly, secure, sameSite set. If sessions currently live in localStorage, move them to a cookie.

7. Database queries that can’t be injected

The problem. SQL built with string interpolation. Less common than it used to be because ORMs are the default, but generated “raw query” helpers still do it.

grep -rnE "\\$\{.*\}.*(SELECT|INSERT|UPDATE|DELETE)" src/

The fix. Parameterised queries, always. Every ORM and driver supports them.

8. Security headers and HTTPS

The problem. HTTPS not enforced; no Strict-Transport-Security, no X-Content-Type-Options, no Content Security Policy. Most hosts handle HTTPS; almost none set the headers.

The fix. Redirect HTTP to HTTPS at the proxy, then add the headers. A CSP is the most work and the most valuable — start with default-src 'self' and widen it as things break.

What this is and isn’t

Fixing these eight makes you safer than most apps at your stage. It is not a penetration test, and it doesn’t make you compliant with anything. If a customer needs SOC 2 or a signed pen-test report, that’s a specialist firm — and it’s worth knowing the difference before someone asks.

Mistakes to avoid

  1. Fixing the one endpoint you tested. If one route lacks an ownership check, the ones written the same way lack it too.
  2. Deleting a leaked key instead of rotating it. It’s still in history and still valid.
  3. Adding a WAF and calling it done. Most of these are logic errors. A firewall doesn’t know what a user is allowed to see.
  4. Validating on the client and feeling safe. The client is the attacker’s machine.
  5. Skipping the tests. Reading code finds some of this. Trying to break it finds the rest.

Checklist

  • Every endpoint that touches a record checks ownership, tested as a second user
  • No secret in the client bundle; git history checked; anything leaked rotated
  • Prices and quantities looked up server-side; payment success confirmed from the provider
  • Every request body validated on the server with a schema
  • Rate limits on login, signup, reset, upload and paid-API routes
  • Reset tokens single-use and short-lived; sessions in httpOnly cookies
  • No interpolated SQL
  • HTTPS enforced, security headers set

If you want this done by someone who’s found these problems many times before, an AI code review covers all eight — with the tests, not just the reading.

← All insights

Show me what’s stuck.

Don’t spend another six hours fighting the same bug. Send me what you’ve got — the messy version is usually the useful version.

Both open with a short template already filled in.