Platform Teardown · Cursor

Is your Cursor-built app actually safe for real users?

Cursor accelerates coding but doesn't audit the code it generates. Here are the production gaps that appear most often in Cursor-built apps.

Default stack: varies · varies

Cursor produces code that reads like code a careful engineer wrote, which is the whole point and also the specific hazard. Reviewing generated code is nothing like reviewing a colleague's: there is no author whose habits you know, no commit message explaining the trade-off, and no moment where someone hesitated. Everything arrives equally confident, including the parts that are wrong.

The failures that survive review are the ones with no visible symptom. A webhook handler that parses a payload but never verifies its signature works perfectly in every test you run, because your tests send real payloads. An auth middleware applied to eleven of twelve routes passes every click-through, because you click the eleven.

The checks below target exactly that category: correct-looking code with a missing verification step. None of them are about style, and none will be caught by a linter.

The part that breaks first

Cursor writes confident, plausible code — including a Stripe webhook that parses the body but never verifies the signature. It looks done. It passes your manual test. And it lets anyone on the internet POST a fake 'payment succeeded' event straight into your database.

And then this becomes your life

So now you read every diff twice. You don't trust the auth middleware until you've clicked every route yourself. You keep a mental list of 'things the AI probably got subtly wrong' — and that list is the real product you're maintaining.

Cursor made you feel like a 10x developer. Then the app went live and you became a full-time code reviewer for an author who never sleeps, never explains itself, and confidently ships race conditions you only find when two users hit the same row.

The good news: every one of these is findable, and most are fixable fast. Here's exactly what tends to be wrong in Cursor apps.

Common gaps in Cursor apps

These aren't hypothetical — they show up in the free audits. See the full production-readiness guide →

#GapSeverityWhere to look
1Stripe webhook without signature verificationCriticalWebhook route handler
2Auth middleware applied inconsistentlyCriticalRoute registration / middleware config
3Read-modify-write without a transactionHighDatabase write paths
4Payment calls without idempotency keysHighPayment integration
5Error responses that leak internalsMediumError handling middleware

1.Stripe webhook without signature verification

Critical

Stripe signs every webhook it sends, and the SDK's constructEvent both verifies that signature and parses the payload. A handler that calls JSON.parse on the body instead skips the verification step and cannot tell a real Stripe event from one anybody POSTed — including a checkout.session.completed that unlocks paid features for free.

How to check yours

Confirm the handler verifies before it trusts, and that it reads the raw body — verification fails against a body that a framework has already parsed and re-serialized. Then prove it: POST a hand-made event with no valid signature and confirm you get a 400 rather than a granted entitlement.

bash
# Does the handler verify at all?
grep -rn 'constructEvent\|STRIPE_WEBHOOK_SECRET' app/ src/ pages/ 2>/dev/null

# Forge an event — a correct handler answers 400
curl -i -X POST https://your-app.example/api/stripe/webhook \
  -H 'Content-Type: application/json' \
  -d '{"type":"checkout.session.completed","data":{"object":{"id":"cs_fake"}}}'

2.Auth middleware applied inconsistently

Critical

Generated code protects the route it was asked about. Over a few sessions that produces a codebase where most routes check the session and some do not, with no pattern to the exception. One unprotected route that returns user data is a breach regardless of how many protected ones sit next to it.

How to check yours

Enumerate every route the app serves, then request each one with no session and record the status. The list of routes that answer 200 is your actual public API. Do this from the deployed URL, and re-run it whenever routes are added — this is the check most worth automating.

bash
# List every route the app serves
find app -name 'route.ts' -o -name 'route.js' | sed 's|/route\..*||'

# Hit each unauthenticated and record the status
for r in /api/users /api/orders /api/admin/stats; do
  printf '%s -> ' "$r"
  curl -s -o /dev/null -w '%{http_code}\n' "https://your-app.example$r"
done

3.Read-modify-write without a transaction

High

Reading a value, computing a new one in application code, and writing it back is correct exactly until two requests do it at once — then one of the two updates disappears. Credit balances, inventory counts, and usage meters are the usual casualties, and the bug is invisible until you have concurrent users.

How to check yours

Find the read-then-write pairs and check whether anything makes them atomic: a transaction, a database-side increment, or a conditional update on the previous value. Then reproduce it — fire the same request twice in parallel and compare the result against what two sequential requests would have produced.

bash
# Two concurrent writes to the same row
curl -s -X POST https://your-app.example/api/credits/spend -d '{"amount":1}' &
curl -s -X POST https://your-app.example/api/credits/spend -d '{"amount":1}' &
wait
# Then read the balance: did it drop by 2, or by 1?

4.Payment calls without idempotency keys

High

Networks retry. A charge request that times out after the payment provider received it will be retried by your client library or by your user hitting the button again, and without an idempotency key the provider treats the retry as a second, separate charge.

How to check yours

Check that every mutating payment call passes an idempotency key derived from something stable — an order id, not a random value generated per attempt, which defeats the point. Then send the same request twice with the same key and confirm the provider returns the original result instead of creating a second charge.

bash
grep -rn 'idempotencyKey\|Idempotency-Key' app/ src/ lib/ 2>/dev/null

5.Error responses that leak internals

Medium

A generated catch block that returns the error object gives the caller your stack trace, file paths, dependency versions, and sometimes the failing query with its parameters. Individually that is embarrassing; together it is a map of your application handed to whoever is probing it.

How to check yours

Trigger a failure on purpose — malformed input, a missing required field — and read the whole response body. Users should get a message and a correlation id. Stack traces and query text belong in your logs, not the response.

bash
curl -s -X POST https://your-app.example/api/items \
  -H 'Content-Type: application/json' \
  -d '{"broken":' | head -c 800

Cursor checklist

Work through these in order before you share the URL publicly. The items marked critical are the ones that expose data to people who never had an account.

  1. 1.

    Stripe webhook uses constructEvent with the webhook secret and the raw bodycritical

  2. 2.

    Every route enumerated and tested unauthenticated against the deployed URLcritical

  3. 3.

    Read-modify-write paths use transactions or database-side atomic updatescritical

  4. 4.

    Payment calls send a stable idempotency key derived from the ordercritical

  5. 5.

    Error responses return a message and correlation id, never a stack trace

  6. 6.

    Concurrency reproduced with parallel requests, not assumed

Cursor production-readiness FAQ

The questions that come up most often about Cursor apps in production.

Does Cursor write secure code?

Cursor writes plausible code, which is not the same thing. It reliably produces the feature you asked for and unreliably produces the verification steps around it — signature checks, transactions, idempotency keys. Those omissions have no visible symptom in testing, so they survive review unless you look for them specifically.

How do I verify my Stripe webhook is secure?

Confirm the handler calls stripe.webhooks.constructEvent with your webhook secret and the raw request body — verification fails if a framework parsed and re-serialized the body first. Then prove it by POSTing a hand-made event with no valid signature. A correct handler answers 400; anything else means unsigned events are being trusted.

Why do AI-generated apps have race conditions?

Because read-modify-write is the natural way to express 'subtract one from the balance', and it is correct until two requests run at once. Generated code optimizes for the single-request path, so the transaction or atomic update that makes it safe under concurrency rarely appears. Reproduce it with two parallel requests before assuming you do not have it.

What is an idempotency key and do I need one?

It is a value you send with a payment request so the provider can recognize a retry of the same operation rather than treating it as a new one. You need it because networks retry: a charge that times out after the provider received it gets sent again, and without the key that is a second charge. Derive it from the order id, not from a fresh random value per attempt.

How do I find routes that are missing auth checks?

Enumerate every route the app serves — for a Next.js App Router project, find every route.ts under app/ — then request each one with no session and record the status code. The routes that answer 200 are your real public API. It is a two-minute loop and the single check most worth wiring into CI, because new routes reintroduce the gap.

Should error responses include the stack trace?

No. A stack trace tells the caller your file paths, dependency versions, and sometimes the failing query and its parameters. Return a human-readable message and a correlation id the user can quote to you, and keep the detail in your logs where you can still search it.

Related

Find out before your users do.

Get the free audit. Three quick fields, a written report personally reviewed by Shane Jordan — not a scanner. You'll know exactly what's wrong in your Cursor app and what to fix first.

Request Free Audit

I take a limited number of audits at a time · priority review available

Operational IntelligenceOur newsletter on operational AI, in your inbox.
Subscribe free →