
so I've been building ShipSafe — security scanner for AI-generated code — and a few weeks ago I got...
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.
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);
}
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.
},
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%.
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();
}
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.
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 });
}
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.
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...");
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.
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.
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
cursorCursor Automations in 2026: wire event-triggered coding agents to Slack, CI, and...
aiThe specs exist. The AI just can't see them. I've always been the type who builds hobby...
amazonbedrockConnect Claude Code, Cursor and Codex to Amazon Bedrock's new console (2026) Summary. On 5...
aiThere is a weird uncanny valley with LLM-generated UI right now. The code functions perfectly, but if...
aiI went down a rabbit hole this morning reading the late-2025 Juejin AI roundups side by side, and the...
mcpInstall guide and config at curatedmcp.com Zendesk MCP: Let Claude Handle Your Support...
Workflows from the Neura Market marketplace related to this Cursor resource