Key takeaways
- Vibe coding moves the bottleneck from writing code to reviewing it. Security problems come from accepting changes nobody read closely.
- AI-written code fails in predictable ways: secrets in client code, missing authorisation checks, string-built queries, permissive defaults, and dependencies that are outdated or do not exist.
- Most of these are cheap to catch if you look at the right moment: before you accept a change, before you merge, and before you deploy.
- Automate what is pattern-shaped (injection, secrets, vulnerable or suspicious packages, misconfiguration) and spend human review on what is not (who is allowed to do what).
- Your coding agent is part of your attack surface too. Review the MCP servers and tools it can use, and do not let it run commands unattended.
What vibe coding changes about security
"Vibe coding", a phrase popularised by Andrej Karpathy in early 2025, describes building software by describing what you want to an AI assistant and accepting what it produces, often without reading every line. Tools such as Cursor, Claude Code, GitHub Copilot and Windsurf have made it the default way many prototypes, and a growing share of production code, get written.
The security problem is not that AI writes uniquely bad code. It is that AI writes plausible code quickly, and plausible code passes a quick glance. The assistant optimises for "it works when I try it", which is exactly the test that does not exercise authorisation, hostile input or a misconfigured deploy. The result is a familiar set of flaws, produced faster than review keeps up.
The flaws AI-written code repeats
1. Secrets in the wrong place
Assistants make the error go away by putting the key where the code can see it: hardcoded in a file, committed in an example .env, or in a browser-exposed environment variable. In Next.js, any variable prefixed NEXT_PUBLIC_ is compiled into the JavaScript every visitor downloads; Vite's VITE_ prefix does the same.
# .env.local
NEXT_PUBLIC_AI_API_KEY=<secret key> # shipped to every visitor's browser// app/api/summarise/route.ts runs on the server; the key never reaches the browser.
// Add authentication and a rate limit before shipping it.
export async function POST(req: Request) {
const { text } = await req.json();
const res = await fetch("https://api.example-ai.com/v1/summarise", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.AI_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ text: String(text).slice(0, 8000) }),
});
return Response.json(await res.json());
}If a key was ever committed, removing it in a later commit is not enough: it is still in the git history. Rotate it.
2. Authentication without authorisation
Ask for "an endpoint to get an invoice" and you will usually get one that checks the user is signed in, but not that the invoice belongs to them. This is an insecure direct object reference (IDOR), and it is the most common serious flaw in AI-built apps because the happy-path test passes.
@app.get("/invoices/{invoice_id}")
def get_invoice(invoice_id: int, user: User = Depends(current_user)):
return db.get(Invoice, invoice_id)@app.get("/invoices/{invoice_id}")
def get_invoice(invoice_id: int, user: User = Depends(current_user)):
invoice = db.get(Invoice, invoice_id)
if invoice is None or invoice.account_id != user.account_id:
raise HTTPException(status_code=404) # same answer as "not found"
return invoiceThe same applies to updates and deletes, to admin-only actions, and to database-as-a-service setups where the assistant turned off row-level security to get a query working.
3. Injection from string-built queries and commands
Assistants still produce f-strings and template literals for SQL, shell commands and file paths, especially in quick scripts that later become endpoints.
# Vulnerable: the email is spliced into SQL
cur.execute(f"SELECT id, password_hash FROM users WHERE email = '{email}'")
# Better: a parameterised query
cur.execute("SELECT id, password_hash FROM users WHERE email = %s", (email,))4. Insecure defaults that make the demo work
When something fails in development, the fastest fix is often to open it up: allow every origin, turn on debug, make the storage bucket public, disable certificate verification, give the service account admin. Those changes rarely get reverted.
# Vulnerable: every website may call the API, with the user's cookies
# (depending on the framework, credentials plus a wildcard reflects any origin)
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True,
allow_methods=["*"], allow_headers=["*"])
# Better: name the origins you serve
app.add_middleware(CORSMiddleware, allow_origins=["https://app.example.com"],
allow_credentials=True, allow_methods=["GET", "POST"],
allow_headers=["Content-Type", "Authorization"])Infrastructure written by an assistant (Dockerfiles, Terraform, Kubernetes manifests) has the same habit: containers running as root, security groups open to 0.0.0.0/0, storage without encryption or with public access.
5. Outdated, vulnerable or non-existent dependencies
Models learned from code written before their training cut-off, so they suggest library versions that were current then and may have known vulnerabilities now. They also sometimes suggest packages that do not exist, with plausible names. Attackers register those names and wait; this is sometimes called slopsquatting. A one-line npm install or pip install of a hallucinated package runs the attacker's code on your machine.
- Check that every new package exists, is the one you meant, has a real maintainer and history, and is not a near-miss of a popular name.
- Install the latest supported version, not the one the assistant remembered, and commit the lockfile.
- Scan dependencies for known vulnerabilities and malicious-package advisories on every change.
6. Validation and limits that only exist in the browser
Generated front ends often validate input carefully while the API behind them accepts anything. Login, signup, password reset and AI endpoints frequently ship with no rate limit, which invites credential stuffing and very large model bills.
- Validate every input on the server with a schema, whatever the front end does.
- Rate-limit authentication and AI endpoints per user and per IP.
- Return generic errors; log the detail on the server instead of sending stack traces to the client.
7. An over-trusted coding agent
The assistant itself has access: to your files, your terminal, your credentials and the MCP servers you connect. A prompt injection in a README, an issue or a web page the agent reads can steer it. Treat agent permissions like any other privileged access: do not auto-approve shell commands, pin and review the MCP servers you add, and keep production credentials out of the environment the agent runs in. Our MCP server security guide covers the details.
The vibe coding security checklist
Use it at five moments. Each check takes minutes, and most can be automated.
| Check | Why |
|---|---|
| Tell the assistant your security constraints up front (auth model, tenant isolation, validation library) | It follows explicit constraints far better than implied ones |
| Keep a project rules file with those constraints, and review it like code | Rules files steer every later change |
| Run the agent without production credentials or broad cloud access | Limits the damage of a bad or injected command |
| Check | Why |
|---|---|
| Read every route or handler that was added or changed | Authorisation bugs hide in code that works |
| Every object load checks ownership or role | Stops IDOR, the most common serious flaw |
No secrets in code, examples or NEXT_PUBLIC_ / VITE_ variables | Browser bundles and git history are public enough |
| Queries, commands and paths are parameterised, not string-built | Prevents injection |
| Every new package exists, is intended and is current | Prevents slopsquatting and known CVEs |
| Check | Why |
|---|---|
| Static analysis (SAST) runs on the pull request | Catches injection and unsafe patterns automatically |
| Dependency scanning (SCA) runs, including malicious-package checks | Catches vulnerable and suspicious packages |
| Secret scanning covers the diff and history | A key removed later is still exposed |
| Tests cover the unhappy path: another user, no permission, hostile input | Generated tests usually only cover success |
| A human reviews authorisation, data access and anything touching money | Scanners do not know your business rules |
| Check | Why |
|---|---|
| No wildcard CORS with credentials; debug mode off | Common demo-only settings |
| Storage buckets, databases and admin panels are not public | Generated infrastructure defaults open |
| Containers do not run as root; security groups are narrow | Limits blast radius |
| Rate limits on login, signup, reset and AI endpoints | Stops credential stuffing and runaway costs |
| Security headers and HTTPS everywhere | Basic browser protections |
| Check | Why |
|---|---|
| Rescan on every change to the default branch | New code, new findings |
| Watch dependencies for new advisories | Yesterday's safe version can become vulnerable |
| Keep an inventory of the AI providers, models and MCP servers in use | You cannot secure what you do not know about |
Automate the pattern-shaped checks
Half of the checklist is pattern recognition, which is what scanners are good at. Put them on every pull request so the assistant's speed does not outrun review:
- SAST for injection, unsafe deserialisation, weak cryptography and similar patterns, mapped to CWE identifiers.
- SCA for vulnerable dependencies, prioritised by exploit signals (CISA KEV and EPSS) rather than severity alone, plus checks for known malicious packages and typosquat-style names.
- Secret detection across the code and its git history.
- IaC checks for Terraform, Kubernetes, CloudFormation and Dockerfiles.
- AI pull request review that combines those deterministic findings with AI-assisted review and a quality gate you can require in branch protection.
CodeStax runs all of these in one scan for GitHub, GitLab and Bitbucket repositories, and its AI security scanning also inventories the AI providers, models, agents and MCP servers your code uses.
Common questions
What is vibe coding?
Vibe coding is building software by describing what you want to an AI assistant and accepting the code it generates, often without reading all of it. The phrase was popularised by Andrej Karpathy in early 2025.
Is AI-generated code secure?
Not by default. It often works on the happy path while missing authorisation checks, input validation and secure configuration, and it can suggest outdated or non-existent packages. Review it and scan it like any other code, with extra attention to access control.
What is slopsquatting?
Slopsquatting is registering package names that AI assistants hallucinate, so that developers who install a suggested but non-existent package get the attacker's code instead. Verify every new dependency before installing it.
What are the most common security flaws in AI-written code?
Secrets in client code or git history, missing authorisation checks (IDOR), string-built SQL and shell commands, permissive defaults such as wildcard CORS or public storage, vulnerable or hallucinated dependencies, and validation or rate limits that exist only in the browser.
How do I secure code written with Cursor, Claude Code or Copilot?
Give the assistant explicit security constraints, read every changed route, run SAST, dependency and secret scanning on each pull request, test the unhappy paths, and limit what the coding agent itself can access and run.