Skip to main content
All posts
CursorSecurityResearch

Is Cursor Safe? What the Research Actually Shows

Published research (arXiv preprint, not yet peer-reviewed) on 200 deployed AI-built apps found 90% carried at least one vulnerability, with broken access control in 75.5%. Here's what that means for Cursor-generated code, and how to fix the common classes.

6 min read

Cursor has become the go-to AI coding environment for thousands of developers. It writes entire functions, scaffolds full-stack apps, and can feel like pair programming with a senior engineer. But there is a critical question most developers skip: is the code it generates actually secure?

Editor's note — this article was corrected

This post originally led with our own figure: 67% of 100 Cursor-built apps we scanned. We pulled that number, along with our Lovable and Replit rates, because we could not produce the corpus or the scan output on request. A verifier that cannot show its working has nothing to sell. The numbers below now come from published research (arXiv preprint, not yet peer-reviewed), cited inline. Read the full retraction.

The best available answer comes from a June 2026 audit of 200 randomly sampled deployed AI-built apps, of which 90% carried at least one vulnerability. This aligns with a Stanford University study which found that developers using AI code assistants produce significantly less secure code, and are more likely to believe it is secure (Perry et al., Stanford 2022).

1. Insecure Direct Object References (IDOR)

IDOR (CWE-639) happens when an API endpoint accepts a user-supplied ID and returns or modifies data without verifying that the requesting user actually owns that resource.

Cursor can generate this pattern because it optimizes for functionality, not authorization. When you ask it to “create an API to get user profile by ID,” it does exactly that — without scoping the query to the authenticated user.

Vulnerable Code (Cursor-generated)
// API route: /api/invoices/[id]
export async function GET(req, { params }) {
  const invoice = await db.invoice.findUnique({
    where: { id: params.id },
  });
  return Response.json(invoice);
}
// Any authenticated user can access ANY invoice
// by simply changing the ID in the URL
Secure Version
// API route: /api/invoices/[id]
export async function GET(req, { params }) {
  const session = await getServerSession();
  if (!session) return Response.json({ error: "Unauthorized" }, { status: 401 });

  const invoice = await db.invoice.findUnique({
    where: {
      id: params.id,
      userId: session.user.id, // Scope to current user
    },
  });

  if (!invoice) return Response.json({ error: "Not found" }, { status: 404 });
  return Response.json(invoice);
}

The fix is straightforward: always include an ownership check in the database query. Every findUnique, update, and delete call should include a userId or organizationId constraint tied to the authenticated session. This is listed as OWASP Top 10 #1: Broken Access Control.

2. Inverted Authentication Conditions

This is a subtle but devastating bug. Cursor sometimes generates authentication guards with the conditional logic reversed — granting access when the user is not authenticated and blocking them when they are.

It shows up in middleware files, API route handlers, and React component guards. The issue stems from how LLMs handle negation in natural language. When you say “only allow authenticated users,” the model sometimes interprets this as checking for the absence of a session rather than its presence.

Vulnerable Code (Cursor-generated)·middleware.ts
// middleware.ts
export function middleware(req) {
  const token = req.cookies.get("session");

  // BUG: This allows unauthenticated users through!
  if (token) {
    return NextResponse.redirect("/login");
  }

  return NextResponse.next();
}
Secure Version·middleware.ts
// middleware.ts
export function middleware(req) {
  const token = req.cookies.get("session");

  // Correct: redirect if NO token
  if (!token) {
    return NextResponse.redirect("/login");
  }

  return NextResponse.next();
}

A single missing ! operator can expose every protected route in your application. This is especially dangerous because it passes basic manual testing — if you test while logged in, everything works fine. The vulnerability only manifests when accessed without authentication.

3. Frontend-Only Role Checks

Cursor can generate role-based access control entirely in the frontend. It will conditionally render admin panels, hide buttons, or redirect users based on a role field stored in the client-side session — but the underlying API endpoints accept requests from anyone.

Vulnerable Pattern
// Frontend component - role check only here
function AdminPanel() {
  const { user } = useAuth();
  if (user.role !== "admin") return <Redirect to="/" />;
  return <DangerousAdminTools />;
}

// API route - NO role check!
export async function DELETE(req, { params }) {
  await db.user.delete({ where: { id: params.id } });
  return Response.json({ success: true });
}
// Anyone can call DELETE /api/users/[id] directly
Secure Version
// API route - enforce role server-side
export async function DELETE(req, { params }) {
  const session = await getServerSession();
  if (!session) return Response.json({ error: "Unauthorized" }, { status: 401 });
  if (session.user.role !== "admin") {
    return Response.json({ error: "Forbidden" }, { status: 403 });
  }

  await db.user.delete({ where: { id: params.id } });
  return Response.json({ success: true });
}

The rule is simple: the frontend controls what users see, the backend controls what users can do. Every privileged action must be verified server-side. This maps to CWE-862: Missing Authorization.

4. Hardcoded Secrets

When you ask Cursor to set up an integration — Stripe, SendGrid, Supabase, OpenAI — it can generate code with placeholder secrets that developers then replace with real keys and forget to move to environment variables. Even worse, sometimes Cursor pulls actual API keys from the context window if they appear in other files.

Vulnerable Code
// lib/stripe.ts
import Stripe from "stripe";

export const stripe = new Stripe(
  "sk_live_51N8x...", // Hardcoded secret key
  { apiVersion: "2024-04-10" }
);

// lib/supabase.ts
const supabase = createClient(
  "https://abc.supabase.co",
  "eyJhbGciOiJIUzI1NiI..." // Service role key in client code!
);
Secure Version
// lib/stripe.ts
import Stripe from "stripe";

export const stripe = new Stripe(
  process.env.STRIPE_SECRET_KEY!,
  { apiVersion: "2024-04-10" }
);

// lib/supabase-server.ts (server-only!)
import "server-only";
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);

If these secrets are committed to a Git repository — even a private one — they become a permanent part of the Git history. According to GitGuardian's 2024 State of Secrets Sprawl report, over 12.8 million new secret occurrences were detected on public GitHub repos in a single year.

What the Research Found

90%

Had ≥1 vulnerability

75.5%

Broken access control

200

Deployed apps audited

κ=0.87

Reviewer agreement

Source: Deng, Fan & Meng, Understanding the (In)Security of Vibe-Coded Applications (arXiv:2606.23130, June 2026). Exploitability was confirmed against the live site by two security reviewers working independently.

For contrast, the study puts the OWASP baseline incidence for broken access control at 3.74%. Note that is a per-category average across all web applications rather than a share-of-apps figure, so it is not strictly commensurable with the 75.5% above — but the gap is wide enough that the direction is not in doubt.

Why Cursor Generates Insecure Code

This is not a Cursor-specific problem. It is an LLM problem. Large language models are trained on millions of code repositories, and the majority of open-source code does not follow security best practices. The model learns to produce code that works — not code that is secure.

There are three structural reasons this happens:

  • Training data bias: Most GitHub code prioritizes functionality over security. The model replicates this bias, generating the “happy path” and skipping defensive checks.
  • Missing security context: Unless your prompt explicitly mentions security requirements, Cursor has no reason to add authorization checks, rate limiting, or input validation.
  • Context window fragmentation: Cursor works file-by-file. It may generate a secure API route but miss that the middleware in another file already handles (or fails to handle) authentication differently.

For a deeper look at why all AI code assistants share this problem, read our guide on AI-generated code security risks.

How to Secure Your Cursor-Generated Code

You do not have to stop using Cursor. But you do need to add a security step to your workflow. Here are five concrete actions:

  1. Add security instructions to your .cursorrules file. Tell Cursor to always add authentication checks, use parameterized queries, and load secrets from environment variables. This significantly improves output quality.
  2. Review every auth-related code path manually. Middleware, API route guards, and role checks deserve line-by-line review. Automated code generation and manual auth review is a strong combination.
  3. Run a security scan before every deploy. Use ShipSafe to scan your repository. It catches IDOR, auth logic bugs, exposed secrets, and injection vulnerabilities in under two minutes.
  4. Use server-only imports for sensitive operations. In Next.js, import "server-only" at the top of files containing secrets or admin logic. This prevents accidental client-side bundling.
  5. Follow our security checklist. We created a 20-item security checklist for vibe coding that covers secrets, auth, injection, XSS, and configuration. Pin it next to your monitor.

Conclusion

Cursor is a remarkable tool. It makes developers dramatically faster. But speed without security is a liability. A 90% vulnerability rate across deployed AI-built apps is not a reason to abandon AI coding — it is a reason to add automated security checks to your workflow.

The developers who will win in the AI era are not the ones who reject AI tools or the ones who blindly trust them. They are the ones who use AI to write code fast and then verify it before shipping. That is exactly what ShipSafe was built for.

You just read what AI builders ship. Now check yours.

Paste your GitHub URL. 2 minutes. You get the findings in plain English, and the report says what it could not check — free, no card.

Scan My App Free