ProductionReady
productionready
FROM PROTOTYPE TO PRODUCTION
← Blog

The 7 issues we find in every Cursor-built app

By Herbert Samuels · Founder of ProductionReady.co. Thirty years in software development, architecture, cloud infrastructure, and IT management across marketing technology, advertising, healthcare, and financial services.

Cursor is probably the strongest AI coding tool available right now. Its code quality is consistently higher than anything else we review, but Cursor-built applications still have a recognizable security profile.

Across the Cursor projects we audit, the same seven production issues recur regardless of whether the builder is experienced or relatively new to software development.

They aren't exotic edge cases. They're basic controls that tend to be missing from otherwise competent, readable code.

1. Authentication is cosmetic

Cursor generates login pages that look complete. There's a form, fields validate, tokens get issued. But dig into the implementation and the problems appear quickly.

Session tokens that never expire are the most common finding. A user logs in once, and that token works forever, even if the account is compromised, even if the user changes their password. We also frequently find tokens stored in localStorage rather than HTTP-only cookies, making them accessible to any JavaScript running on the page. One XSS vulnerability, and every active session is compromised.

Rate limiting on authentication endpoints is almost never present. An attacker can attempt thousands of password combinations without triggering any protection. Password reset flows are similarly exposed: tokens are predictable, don't expire, and aren't single-use.

2. Environment variables are an afterthought

Cursor will generate code that uses process.env.DATABASE_URL or process.env.API_KEY, which looks correct. But the actual values are frequently defined in files that get committed to the repository: .env files without a corresponding .gitignore entry, config.js files with production credentials, or docker-compose.yml files with database passwords in plain text.

The code can therefore look as though it uses environment variables while still exposing the underlying secrets to anyone with repository access. If the repo is public, or becomes public accidentally, every connected service is compromised simultaneously.

We've also found cases where Cursor generates both the environment variable reference and a hardcoded fallback value in the same line: const apiKey = process.env.API_KEY || 'sk-live-abc123...'. The developer sees the process.env part and assumes it's handled. The fallback, containing a live production key, is the actual vulnerability.

3. Database queries aren't parameterized consistently

Cursor knows about SQL injection. If you ask it to build a query, it will often use parameterized queries or an ORM. But the consistency breaks down across a project. The main CRUD operations might be properly parameterized while a search endpoint, an admin filter, or a reporting query uses string concatenation.

The explicitly requested queries are often handled correctly. The supporting queries (the ones Cursor generated as part of a larger feature) often aren't. It only takes one unparameterized query to compromise the entire database.

4. Error handling exposes internals

When something goes wrong in a Cursor-built app, the error response typically includes information an attacker shouldn't have: full stack traces, database table names, file system paths, library versions, and sometimes fragments of the query that failed.

This happens because Cursor generates try/catch blocks that send the raw error object to the client. The code handles errors in the sense that it doesn't crash, but it handles them by giving the user (or an attacker) a detailed map of the application's internal structure.

In production, error responses should be generic and consistent. Log the details server-side. Return a status code and a human-readable message. Nothing more.

5. No request validation layer

Cursor generates API endpoints that trust incoming data. If the endpoint expects an email address, it processes whatever string arrives. If it expects a numeric ID, it passes the input to the database without confirming it's actually a number.

This is about application stability, not just SQL injection. Invalid data types cause unexpected errors. Oversized payloads consume memory. Missing required fields produce confusing downstream failures. A proper validation layer (schema validation on every endpoint) prevents an entire category of bugs and vulnerabilities simultaneously.

The pattern we see is that Cursor validates data on the frontend (form fields that check email format, character limits on text inputs) but skips server-side validation entirely. Frontend validation is a user experience feature. Server-side validation is a security feature. They're not interchangeable.

6. Authorization is missing or flat

Authentication checks whether someone is who they claim to be. Authorization checks whether they're allowed to do what they're trying to do. Cursor reliably generates the first and almost never generates the second.

The most common manifestation: every authenticated user can access every other user's data by manipulating IDs in the URL or API request. User A fetches /api/documents/123 and gets their document. They change it to /api/documents/456 and get User B's document. The server checks that the request comes from an authenticated user. It doesn't check that the user owns the requested resource.

We call these Insecure Direct Object References, and they're present in roughly eight out of ten Cursor-built apps we audit. They're also among the easiest vulnerabilities to exploit. No tools required, just a browser and curiosity.

7. No rate limiting anywhere

Cursor doesn't add rate limiting to any endpoint unless explicitly asked. This means every API route in the application will process unlimited requests from any source at any speed.

The consequences compound. Without rate limiting on authentication, credentials can be brute-forced. Without rate limiting on data endpoints, the entire database can be scraped. Without rate limiting on resource-intensive operations, a single client can effectively denial-of-service the application by triggering expensive queries or file processing repeatedly.

Rate limiting is only a few lines of middleware in most frameworks, yet it's one of the most effective controls against automated abuse. Its absence is a good example of why working application code still needs a separate production-security review.

What to take from these findings

None of these findings makes Cursor a poor tool. Cursor is very good at generating functional code from natural-language instructions. Production hardening is simply a different job from feature development, and it needs to be treated as one.

Each issue above has a well-understood remediation: proper session controls, safe query construction, server-side validation, authorization checks, rate limiting, secret management, and structured error handling. Once someone deliberately looks for them, they're usually straightforward to correct.

If a Cursor-built application is moving toward real users or sensitive data, a focused security review can identify exactly which of these controls are missing without forcing a rewrite of the working product.

The application can be well built and still be incomplete from a production-security standpoint. That distinction is the reason for the review.

Free Production Readiness Check

See where your app stands in 5 minutes.

16 questions across 8 categories: authentication, secrets, data layer, API security, and more. Get a scored report with prioritized recommendations.

ProductionReady
productionready
FROM PROTOTYPE TO PRODUCTION
Security audits, hardening, and backend builds for vibe-coded applications.
Company