Security in Vibe Coding: The most common vulnerabilities and how to avoid them
Generative coding is exceptionally fast and efficient. With a simple prompt requesting an endpoint, you can have routes, business logic, and database queries wired together in seconds. The code runs, tests pass, and CI is green.
When errors do occur, they're quickly fixed by highlighting the terminal error, letting the model interpret it, and implementing the fix—often before you've had time to analyze the issue yourself.
This speed comes with security risks. AI-generated code can introduce production-grade vulnerabilities that bypass traditional SAST tools, survive CI/CD pipelines, and reach production. These include remote code execution vulnerabilities, authentication bypasses, and logic flaws—all in code that appears functional and correct at first glance.
What Is Vibe Coding
The term "vibe coding" originated from a tweet by Andrej Karpathy and has since gained significant popularity, even being recognized by Merriam-Webster as trending slang.
Vibe coding extends beyond merely using AI for development—it involves relying on it as the primary development mechanism. Developers prompt an LLM, accept the output wholesale, and proceed without validation or threat modeling.
This approach has become standard practice when developing with tools like Copilot, Cursor, and internal chat-based development agents. While it accelerates iteration, it often produces functionally working code that contains structural security flaws.
What Makes Vibe Coding Risky
Pattern Completion Over Intent
LLMs function by predicting the next most probable token rather than applying security engineering principles. They prioritize fulfilling functional requirements over security considerations.
Consider this prompt: Build me a file upload server using Express and Multer
// imports
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const uploadDir = 'uploads';
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir);
}
cb(null, uploadDir);
},
filename: (req, file, cb) => {
const uniqueName = Date.now() + '-' + file.originalname;
cb(null, uniqueName);
}
});
const upload = multer({ storage });
app.post('/upload', upload.single('file'), (req, res) => {
if (!req.file) return res.status(400).send('No file uploaded.');
res.send(`File uploaded: ${req.file.filename}`);
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
The code works as requested—it handles uploads and provides frontend progress indication.
But critical security measures are absent because they weren't explicitly required:
No file type validation. You can upload .php, .exe, .jsp, or anything else. If this server sits behind NGINX or Apache with misconfigurations, you now have an RCE.
No filename sanitization.
file.originalname
is user-controlled input. A user can send../../../etc/passwd
or../evil.js
, and if you lack sandboxing, you're now facing path traversal.
The code satisfies functional requirements but fails to implement security best practices, creating vulnerabilities that basic testing won't catch.
No Awareness of System Boundaries
LLMs generate code at the function level without considering system-level architectural constraints like session state, service interactions, or permission enforcement.
This lack of architectural thinking leads to broken access controls, missing state checks, and logic flaws that only become apparent in broader contexts.
For example, requesting a document download API:
build an API to allow users to download documents they’ve uploaded.
// ... upload logic ...
// Download route
app.get('/download/:filename', (req, res) => {
const filePath = path.join(__dirname, 'uploads', req.params.filename);
if (fs.existsSync(filePath)) {
res.download(filePath);
} else {
res.status(404).json({ error: 'File not found' });
}
});
The code functions correctly but contains significant security gaps:
- No user context or authentication middleware to associate files with specific users
- No ownership verification allows anyone who knows or guesses a filename to download arbitrary files
- No access model to restrict file access based on permissions
These aren't merely bugs but fundamental design oversights. The model delivers functional code without addressing essential security boundaries.
**Training on Insecure Patterns**:
LLMs primarily learn correlation patterns from massive datasets containing GitHub repositories, tutorials, and StackOverflow threads—sources that often contain more insecure examples than secure ones.
Let's say you prompt:
Write a Flask middleware to verify an HMAC signature header on incoming requests
import hmac
import hashlib
from flask import request, abort
SECRET = b'supersecretkey'
def verify_signature():
signature = request.headers.get("X-Signature")
body = request.get_data()
expected = hmac.new(SECRET, body, hashlib.sha256).hexdigest()
if signature != expected:
abort(401)
At first glance, the code appears correct. It validates signatures similar to webhook validators or API gateways. However, a subtle vulnerability exists:
- The code uses
==
for HMAC comparison instead of a constant-time comparison, creating vulnerability to timing-based side-channel attacks.
This vulnerability likely won't appear in tests or reviews, but could enable gradual brute-forcing of signatures by observing response times.
How to maintain security in vibe coding
While "review all AI-generated code" or "label AI-generated code in your codebase" are sound recommendations, they may not be practical given the productivity benefits of these tools. Here are more realistic strategies:
Integrate code scanning tools or SAST:
SAST refers to Static Application Security Testing. These tools analyze your source code, bytecode, or binaries for vulnerabilities without actually executing the program. Think of them as a security-focused linter. They flag issues like SQL injection, command injection, and insecure function usage directly from your codebase.
Traditionally, while SAST has been really helpful in maintaining security, we at ZeroPath have redesigned the technology to leverage LLMs. This architectural change has reduced the number of false positives and has helped us detect business logic issues in some large open-source repos.
Development Stage: Use code scanning tools that integrate with your AI workflow (inside cursor, windsurf, etc) to catch vulns. You can run your scans on ZeroPath and interact with them by hooking your IDE to ZeroPath's MCP servers.
Production Stage: Integrate SAST into CI/CD pipelines to prevent vulnerabilities from reaching deployment, with tools like ZeroPath that can automatically create pull requests with patches that you can review and merge.
The goal here is to have independent tools verify your code before the end user uses it. If the pipeline has linting and security tests, they should flag things like “input not sanitized” or “insecure HTTP connection”.
Security based prompt engineering:
Enhance AI outputs by explicitly incorporating security requirements in your prompts.
This means explicitly prompting the AI agent to prioritize security in its output. Rather than requesting "write a file upload function," specify "write a secure file upload function that checks file type and size and prevents path traversal."
For IDE tools like Cursor that support rule systems, create security-focused rule files. These are .mdc
files in .cursor/rules
:
---
description: Enforce security in code generation
globs: "**/*"
alwaysApply: true
---
- Validate all user inputs to prevent injection attacks.
- Sanitize and encode outputs to mitigate XSS vulnerabilities.
- Implement proper authentication and authorization checks.
- Use HTTPS for all external communications.
- Avoid hardcoding secrets or credentials in the codebase.
More about the rules file here.
2-Stage AI Prompting:
Implement a simple but effective approach: prompt the AI twice—first to implement the feature, then to review and improve its own output for security issues.
After receiving the initial solution, follow up with: "Now review this code for any security vulnerabilities or mistakes and fix them." This second-pass review often catches obvious security issues, functioning like an integrated static analysis.
Conclusion:
Vibe coding is cool.
It reduces barriers for developers and increases development accessibility. However, its security limitations require vigilance—whether through code review, specialized security tools, or supplementary manual coding.
The security landscape is evolving to integrate with AI code generation, but until that integration matures, developers must implement additional measures to ensure AI-generated code meets security standards.