DEV Community

Tomer goldstein
Tomer goldstein

Posted on • Originally published at ship-safe.co

Is Cursor Safe? I Scanned 100 Apps. 67% Had Critical Vulns.

so I've been building ShipSafe — security scanner for AI-generated code — and a few weeks ago I got curious. like, actually curious. not "I wonder if AI code has bugs" curious, more like "how bad is it really and am I just being paranoid" curious.

I grabbed 100 Cursor-built repos off GitHub. not tutorials, not demo apps. real production stuff — SaaS tools, internal dashboards, a couple e-commerce stores, bunch of API backends. found them by searching for .cursorrules files and Cursor-style commit patterns.

then I scanned all of them with ShipSafe.

67%. sixty-seven percent had at least one critical vulnerability. the worst app had 14 separate issues. fourteen. average was 3.2 per app.

ngl I expected some problems but not... that.

% of apps
had a critical vuln 67%
IDOR 43%
inverted auth 31%
frontend-only admin checks 28%
hardcoded secrets 22%

this tracks with Stanford research that found ~45% of AI-assisted code has vulns. our numbers are worse, probably bc we only looked at shipped production apps vs their lab setup.

anyway. let me show you what I kept finding.


the IDOR thing (43%)

this was by far the most common. and it's so dumb that it's almost funny? Cursor generates an API route, takes an ID from the URL, fetches from the database, returns it. no check on who's asking.

// /api/invoices/[id]
export async function GET(req, { params }) {
  const invoice = await db.invoice.findUnique({
    where: { id: params.id },
  });
  return Response.json(invoice);
}
Enter fullscreen mode Exit fullscreen mode

change /api/invoices/42 to /api/invoices/43. congrats you're reading someone else's invoice now. their name, the amount, payment status, all of it. OWASP literally lists this as vulnerability #1.

the fix btw:

where: {
  id: params.id,
  userId: session.user.id, // this. this is the whole fix.
},
Enter fullscreen mode Exit fullscreen mode

one line. Cursor never adds it. and I get why — the AI is optimizing for "does it work" not "who's allowed to see this." but still. 43%.


the backwards auth thing was the wildest tho

okay 31% of these apps had their auth middleware inverted. and I know that sounds fake but look at this:

export function middleware(req) {
  const token = req.cookies.get("session");

  if (token) {
    return NextResponse.redirect("/login");
  }
  return NextResponse.next();
}
Enter fullscreen mode Exit fullscreen mode

if (token) → kick to login. so logged-in users get redirected away. and if you DON'T have a token? NextResponse.next() — come right in. every protected route, wide open to anyone without a session.

one missing !. that's it. if (!token) vs if (token).

and here's what makes it so nasty — you will never find this by testing your own app. you're logged in while you test. the redirect fires on you and maybe you think "huh that's weird" and hack around it or don't even notice bc your session is cached. meanwhile an attacker shows up with zero cookies and gets full access to everything. no cap the first time I saw this in a real production app I just stared at it for like 30 seconds.


frontend admin checks that do nothing

28% of apps had this pattern where Cursor puts the role check in React but not on the server. and I gotta admit this one annoys me the most bc it's so close to being right.

// this part is fine honestly
if (user.role !== "admin") return <Redirect to="/" />;

// this part is NOT fine
export async function DELETE(req, { params }) {
  await db.user.delete({ where: { id: params.id } });
  return Response.json({ success: true });
}
Enter fullscreen mode Exit fullscreen mode

the admin panel is hidden in the UI. great. but curl -X DELETE /api/users/123 works for literally anyone. the API doesn't check anything. Cursor generated the visual gate and forgot the actual gate.

I started telling people: frontend is what you see, backend is what you can do. Cursor gets the see part right every time. the do part? apparently optional.


hardcoded secrets + a personal L

22% had keys in the source. and tbh this is the one I'm least judgmental about bc I almost did this myself early on.

you're setting up Stripe or whatever. you paste your API key into Cursor's chat for context. Cursor writes the integration and puts the key right there in the file:

export const stripe = new Stripe("sk_live_51N8x...");
Enter fullscreen mode Exit fullscreen mode

you commit, push, and now that key is in your git history permanently. deleting the file doesn't help — git log remembers. GitGuardian says 12.8 million secrets were exposed on GitHub last year and I bet a huge chunk started exactly like this.

I almost shipped a Supabase service role key this way. caught it in a scan literally the day before pushing to prod. that's actually one of the reasons I started building ShipSafe — I needed it for myself first.


why tho

okay so why does Cursor keep doing this? I've thought about it a lot and I think there's two things going on.

first — LLMs learn from GitHub. and most code on GitHub is focused on making things work. security is an afterthought in like 90% of open source repos. so the model generates happy-path code. auth checks, ownership verification, input validation — that stuff doesn't make the app function, it prevents exploitation. the model literally doesn't prioritize it unless you specifically ask.

second thing — and this is the one that's harder to fix — Cursor thinks about one file at a time. it'll generate a totally reasonable API route without knowing that your middleware in some other directory is supposed to handle auth. or that it doesn't. the context window sees the file you're working on, not the security architecture of your whole app.

tbh the second one bugs me more than the first bc you can kinda solve the first one with good .cursorrules but the file-at-a-time thing is structural.


what actually changed my workflow

I'm not gonna stop using Cursor over this. the speed boost is too real. but I did change a few things and it's made a huge difference.

biggest one — I added security-specific rules to my .cursorrules file. things like "always add userId to database where clauses" and "never hardcode secrets, always use process.env." sounds simple but it legit changed the output quality. night and day.

the other thing is I just manually review auth-related code now. everything else, Cursor handles and I trust it. but middleware files, API route guards, anything with role checks — I actually read those line by line. takes like 5 minutes per PR and it's caught issues multiple times.

and obviously I run ShipSafe before deploying. that's the whole reason I built it lol. paste the GitHub URL, get a report, fix the flagged stuff, ship. couple minutes.

oh and I stopped pasting real API keys into Cursor chat. use fakes, swap in real ones through env vars. learned that one the hard way with the Supabase key thing I mentioned.


Cursor makes you stupid fast. I'm not going back to writing everything manually. but "fast" and "secure" aren't the same thing and 67% of production apps having critical vulns is a pretty loud signal.

just add a scan to your deploy flow. like two minutes. saves you the 2am incident response call.


full data + methodology: ship-safe.co/blog/is-cursor-code-secure

free scan: ship-safe.co

Top comments (1)

Collapse
 
aiforwork profile image
Julian Oczkowski

The inverted auth middleware example is a perfect illustration of why AI-generated code needs security review as a first-class concern, not an afterthought. The if (token) vs if (!token) bug is particularly dangerous because it passes basic functional testing — the developer is always logged in during manual QA, so the redirect behavior seems correct from their perspective.

What stands out in your data is that these aren't exotic vulnerabilities. IDOR, missing server-side auth, hardcoded secrets — these are well-known patterns that any senior engineer would catch in code review. The issue is that AI coding assistants optimize for "does it work" rather than "is it secure," and developers trusting the output skip the scrutiny they'd apply to code written by a junior teammate.

This makes a strong case for integrating automated security scanning directly into the AI-assisted development workflow — not as a CI gate after the PR is opened, but as real-time feedback while the code is being generated.