Skip to main content

Security Mindset for Vibe Coding with Cursor

Vibe coding security with Cursor

Vibe coding with Cursor is fast and fun: you describe what you want, the agent writes the code, and you keep the flow going without stopping to read every line. The problem is that a model trained to satisfy prompts will happily produce code that works and looks clean while still carrying SQL injection, hardcoded API keys, or an authorization check that trusts the client. This guide gives you a repeatable security mindset for the vibe-coding workflow in Cursor, from spotting the failures that AI-generated code produces most often to auditing output, enforcing rules, handling secrets, and running a pre-deployment checklist before anything ships.

What Is Vibe Coding and Why Security Matters​

Vibe coding means you steer with intent and let the model fill in the implementation details. You ask for "a login endpoint with a session cookie" and Cursor writes the route, the database call, and the middleware. The output is usually functional, but functional is not the same as secure. The model optimizes for making the code run, not for defending against an attacker who reads the same code.

Security matters more in this workflow, not less, for three reasons:

  • You review less. When the model writes dozens of lines per prompt, the human review step becomes the bottleneck and gets skipped.
  • The model learned from insecure code. Public repositories contain plenty of string-concatenated SQL and client-side token storage, so those patterns show up in completions.
  • The blast radius is large. Agent mode can touch many files at once, so a single insecure pattern gets duplicated across the codebase.

Security is not a separate phase you bolt on at the end. It is a constant habit you build into how you prompt, review, and deploy with Cursor.

You are still the reviewer

Cursor's agent can write code that compiles, passes your tests, and still ships a vulnerability. The model cannot sign off on your risk. Treat every generated block as a draft from a fast but careless junior developer.

The Most Common Vulnerabilities in AI-Generated Code​

These are the failure modes that show up again and again in AI-generated code. Learn to recognize the shape of each one so you can spot it while scanning rather than reading every line.

VulnerabilityWhat it looks like in generated codeWhy the model produces it
SQL injectionQuery built with string concatenation instead of parametersConcatenation is simpler to write and reads "naturally"
XSSUnsanitized user input rendered with innerHTML or dangerouslySetInnerHTMLRendering HTML directly is the shortest path to the visual result
Hardcoded secrets / API keysconst API_KEY = "sk-live-..." left in the source fileA literal value makes the snippet self-contained and runnable
Broken auth / session handlingTokens stored in localStorage, no expiry, no server-side validationClient storage is the easiest to demo in a browser
Permissive CORSAccess-Control-Allow-Origin: * with credentialsWildcard is the one-line fix that stops the immediate error
Unsafe deserializationpickle.loads, eval, or trusting JSON straight into objectsDeserializing everything is convenient and assumes trusted input
Exposed admin endpointsRoutes like /admin/users with no role checkThe model implements the feature but forgets the guard
Missing input validationBody used directly in logic with no schema checkValidation feels redundant when the happy path already works

SQL Injection in JavaScript​

Here is the classic pair: a query that concatenates user input, and the same query written with parameterized placeholders.

// VULNERABLE: user input is concatenated straight into the query
app.get('/user', async (req, res) => {
const sql = "SELECT * FROM users WHERE email = '" + req.query.email + "'";
const rows = await db.query(sql);
res.json(rows);
});
// FIXED: parameterized query, input never becomes part of the SQL string
app.get('/user', async (req, res) => {
const sql = 'SELECT * FROM users WHERE email = $1';
const rows = await db.query(sql, [req.query.email]);
res.json(rows);
});

Authorization Checks in Python​

The vulnerable version trusts a flag sent by the client. The fixed version checks the authenticated session on the server.

# VULNERABLE: the client tells the server whether it is an admin
@app.get("/admin/users")
def admin_users(request):
if request.args.get("is_admin") == "true":
return db.list_users()
return {"error": "forbidden"}
# FIXED: the server decides based on the authenticated session
@app.get("/admin/users")
def admin_users(request):
user = request.session.get("user")
if not user or user["role"] != "admin":
abort(403)
return db.list_users()

Storing a session token in localStorage makes it readable by any script on the page, which turns a single XSS bug into full account takeover.

// VULNERABLE: token readable by any script, no protection against XSS
localStorage.setItem('token', response.token);
// FIXED: server sets an httpOnly cookie that scripts cannot read
res.cookie('session', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 1000 * 60 * 60,
});

Auditing AI-Generated Code with Cursor​

Auditing does not mean reading every line. It means asking a second pass of the model to attack what the first pass wrote. Cursor is good at this when you give it the right persona and a clear target.

A Security-Review Persona Prompt​

Reuse this prompt in Agent mode after the agent finishes a feature. It forces the model to switch from builder to attacker.

You are a security reviewer auditing code a previous agent just wrote.
For every file changed in this feature:
1. List each place that handles user input, authentication, or secrets.
2. For each, describe the concrete attack an unauthenticated user could run.
3. Show the vulnerable line and the fixed line side by side.
4. Rate severity as low, medium, high, or critical with one sentence of reasoning.
Do not rewrite the whole feature. Report findings only.

Asking for Threat Modeling in Agent Mode​

Beyond reviewing finished code, you can ask Cursor to threat-model before it writes. This surfaces design flaws that line-level review misses.

Before implementing this billing endpoint, do a short threat model:
- Who can call this route and with what privileges?
- What happens if the amount, currency, or user ID is tampered with?
- Which trust boundary does this input cross?
Then implement the endpoint with those mitigations in place.

Using .cursor/rules to Enforce Checks​

You can encode the audit itself as a rule so the agent runs it automatically on every change. Rules in .cursor/rules/ are loaded into context, which means the security checklist is always present when the agent works.

Audit after every agent session

Make "run a security review" a reflex after any agent session that touches auth, input handling, or secrets. It costs one prompt and catches the failure modes from the table above before they reach a commit.

Enforcing Security with Cursor Rules​

A project rule file keeps your security expectations in front of the model without repeating yourself. Create .cursor/rules/security.mdc and commit it so every collaborator gets the same guardrails.

---
description: Security requirements for all code in this project
globs: ["**/*.ts", "**/*.tsx", "**/*.py", "**/*.js"]
alwaysApply: true
---

# Security requirements

- Never write SQL by concatenating user input. Use parameterized queries or an ORM.
- Never render user input with innerHTML or dangerouslySetInnerHTML. Escape it or use a safe renderer.
- Never hardcode secrets, API keys, or tokens. Read them from environment variables.
- Never grant administrative access based on a value supplied by the client.
- Always validate request bodies against a schema before use.
- Always set httpOnly, secure, and sameSite on session cookies.
- Never use eval, exec, or pickle.loads on untrusted input.
- When adding a new endpoint, state explicitly which role can call it.
- When touching auth or input handling, add a security-review note in the response.

With alwaysApply: true, the rule is injected into every request, so the agent self-corrects while it writes instead of you catching problems later. You can also scope rules to specific folders with globs so backend security rules do not clutter frontend work.

Managing Secrets and Environment Variables​

Hardcoded secrets are the single easiest vulnerability to ship by accident, because a literal value makes code runnable and the model likes runnable code. Keep secrets out of source entirely.

Environment Variable Patterns​

# .env.example -- commit this, it documents the shape without real values
DATABASE_URL=postgres://user:password@localhost:5432/app
SESSION_SECRET=replace-me
STRIPE_SECRET_KEY=replace-me
# Load locally, never commit the real .env
cp .env.example .env
# .env -- gitignored, contains the real values and stays on your machine
DATABASE_URL=postgres://realuser:realpass@prod-host:5432/app
SESSION_SECRET=a-long-random-value-from-a-secret-manager
STRIPE_SECRET_KEY=sk_live_the_actual_key

Always add .env and related files to .gitignore before your first commit, not after.

# .gitignore
.env
.env.local
.env.*.local
*.pem
*.key

Pre-Commit Secret Scanning with gitleaks​

A pre-commit hook stops a secret before it ever reaches the repository history, which is far cheaper than rotating a leaked key.

# Install gitleaks (Homebrew example)
brew install gitleaks

# Scan the whole repo once
gitleaks detect --source . --verbose

# Or install it as a pre-commit hook
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
pre-commit install
pre-commit run --all-files
Rotate first, clean second

If a real secret ever lands in a commit, rotate the credential immediately. Deleting the line or rewriting history does not un-leak a key that has already been pushed to a remote.

Validating Authentication and Authorization Manually​

Automated scans miss logic bugs, so you still have to verify the flow by hand. Walk through these checks against a running instance.

Session and Auth Checks​

  • Log out, then request a protected route with the old token. It must fail with 401, not return data.
  • Change your own user ID in a URL or body to another user's ID. The server must reject it, not trust the parameter.
  • Call an admin route as a non-admin. It must return 403 regardless of any client-supplied flag.
  • Confirm cookies are set with httpOnly, secure, and an expiry. Open the browser console and try to read the cookie with document.cookie; it should not appear.
  • Reset a password and verify the old session is invalidated.

Token and Header Checks​

# A protected route without credentials must not return data
curl -i https://api.example.com/me

# A protected route with a forged or expired token must fail
curl -i https://api.example.com/me -H "Authorization: Bearer forged.token.value"

# An admin route as a normal user must return 403
curl -i https://api.example.com/admin/users -H "Authorization: Bearer $USER_TOKEN"

Run these against a staging environment that mirrors production, not just against localhost where the data is throwaway.

Dependency and Supply Chain Scanning​

Your own code is only part of the surface. Generated code pulls in dependencies, and a vulnerable package is a vulnerability you shipped. Scan the tree on every change.

# Node / npm -- built in, no extra install
npm audit
npm audit --production

# Python
pip-audit
pip-audit -r requirements.txt
# Snyk for a cross-language view
npm install -g snyk
snyk test
snyk monitor

# Or run the Snyk CLI against a Python project
pip install snyk
snyk test --file=requirements.txt

Wire a scanner into CI so it runs on every pull request rather than relying on memory.

# .github/workflows/security.yml (excerpt)
- name: Scan dependencies
run: npm audit --production --audit-level=high
Pin and review new dependencies

Before accepting a package a model suggested, check its download count, last publish date, and maintainer. Typosquatted packages are a real attack vector, and a model may suggest a name that is close to but not the real library.

A Pre-Deployment Security Checklist​

Run this list before every deploy. It is deliberately short so you will actually use it.

  • No hardcoded secrets, API keys, or tokens in any committed file
  • .env and key files are in .gitignore and were never committed
  • All SQL uses parameterized queries or an ORM, with no string concatenation
  • All user input is validated against a schema before use
  • Session tokens are stored in httpOnly cookies, not localStorage
  • Every protected route returns 401/403 when unauthenticated or unauthorized
  • Admin routes verify role server-side, never from a client-supplied value
  • CORS allows only your known origins, not * with credentials
  • npm audit / pip-audit reports no high or critical findings
  • Error responses do not leak stack traces, SQL, or internal paths
  • Logging does not record tokens, passwords, or full request bodies
  • A security-review pass has run on every file the agent touched this sprint

Reporting and Remediating Findings​

Findings are only useful if they get fixed and tracked. Give each one just enough structure to act on.

  1. Record the finding. One line with the file, the line, the vulnerability type, and the severity.
  2. Write the fix as a diff. Have Cursor produce the vulnerable-line-to-fixed-line change, then review it.
  3. Add a test that proves the fix. A regression test for the injection or the auth bypass keeps it from returning.
  4. Update the rule. If the agent produced the bug, add a line to .cursor/rules/security.mdc so it does not produce it again.
  5. Retest and close. Re-run the manual checks and the scanner against the fixed build.
Finding: CWE-89 SQL injection, src/api/users.ts:42, severity high
Vulnerable: db.query("SELECT * FROM users WHERE id = " + id)
Fixed: db.query("SELECT * FROM users WHERE id = $1", [id])
Regression test: request /user?id=1' OR '1'='1 returns no rows
Rule update: added "never concatenate user input into SQL" to security.mdc
Status: fixed, verified in staging

Treat the rule file as a growing record of every mistake the agent has made. Over time it becomes the most valuable security asset in the repository, because it encodes exactly the ways your codebase has been wrong before.