What is vibe coding?
"Vibe coding" describes the practice of using AI coding assistants (GitHub Copilot, Cursor, ChatGPT, Claude) to generate substantial portions of application code β often by describing intent in natural language and accepting generated completions with minimal review.
The term captures something real: developers using these tools are often in a flow state where they accept suggestions quickly, trusting the AI to handle implementation details. Security is one of those details. And it is one the AI frequently gets wrong.
The research is clear: A Stanford study (2021) found that developers using Copilot wrote significantly more security bugs than those who did not. A follow-up NYU study confirmed the finding. AI coding assistants make developers faster β but not more secure, unless explicit security guardrails are in place.
Why LLMs generate insecure code
LLMs are trained to predict the next token given the previous context. They are optimised for code that looks like the training data β which includes billions of lines of code from public repositories. Public repositories contain enormous amounts of insecure code.
- Training data bias: The majority of code on GitHub is not security-reviewed. Patterns like SQL string concatenation, hardcoded test credentials, and disabled SSL verification are common in the training corpus.
- No semantic understanding of security intent: An LLM generating a database query function does not understand that the user input must never be interpolated directly into SQL. It generates a pattern that looks like database query code.
- Context truncation: LLMs have context limits. When generating code for a function, they may not "see" the security constraints defined elsewhere in the file or project.
- Outdated patterns: Models trained on older data may generate deprecated cryptographic functions, obsolete TLS configurations, or deprecated security APIs.
Pattern 1: Hardcoded secrets in generated code
Ask an LLM to generate a database connection function and it will often produce a working example with placeholder credentials inline. In vibe coding workflows, these placeholders get accepted, "fixed later" (which means never), and eventually committed.
# What the LLM generates (looks fine, compiles fine) def get_db_connection(): return psycopg2.connect( host="db.internal", user="app_user", password="Sup3rS3cr3t!", # β hardcoded, will be committed database="production" ) # What it should generate: import os def get_db_connection(): return psycopg2.connect( host=os.environ["DB_HOST"], user=os.environ["DB_USER"], password=os.environ["DB_PASSWORD"], database=os.environ["DB_NAME"] )
Pattern 2: Injection flaws in generated queries
LLMs generating database query code frequently default to string interpolation because it is the most common pattern in their training data. The parameterised query pattern is less common and requires understanding of the security intent.
# Vulnerable: LLM generates the natural-looking version def get_user(username: str): query = f"SELECT * FROM users WHERE username = '{username}'" cursor.execute(query) # β SQL injection # Secure: parameterised query def get_user(username: str): cursor.execute( "SELECT * FROM users WHERE username = %s", (username,) # β parameterised, safe )
SAST catches this β if you run it: SQL injection via string concatenation is one of the easiest vulnerability classes for SAST to detect. The problem is that vibe coding workflows often skip or defer security scanning. The LLM generates the code; it ships. SAST in CI would catch this in seconds.
Pattern 3: Insecure defaults and disabled security controls
LLMs learn from tutorial code and quick-start examples, which commonly disable security controls for simplicity. In production code, these "for development only" settings are dangerous:
verify=Falsein Python requests β disables SSL certificate verificationDEBUG=Truein Django/Flask settingsallowedOrigins: ["*"]in CORS configurationsalgorithm: 'none'in JWT configurations (we have seen this generated)- MD5 or SHA1 for password hashing instead of bcrypt/argon2
# LLM generates: "here's a simple JWT decode" import jwt payload = jwt.decode( token, options={"verify_signature": False} # β disables all security ) # Secure version: payload = jwt.decode( token, SECRET_KEY, algorithms=["HS256"] # β explicit, verified )
Mitigations: making vibe coding safer
The solution is not to stop using AI coding tools β they provide real productivity gains. The solution is to add security guardrails that catch what the AI misses:
- IDE scanning on every file save: Catches hardcoded credentials and injection patterns as soon as the LLM-generated code lands in the editor.
- Pre-commit hooks: Detect-secrets, Gitleaks, and TruffleHog as pre-commit hooks catch credentials before they ever reach the repository.
- SAST on every PR: The LLM may generate injection flaws; CI SAST will catch them before merge. Non-negotiable.
- Security-aware prompting: When prompting LLMs, explicitly include security requirements: "generate a parameterised query" or "use environment variables for credentials, never hardcode them".
- AI-specific SAST rules: AquilaX includes rules tuned to catch the specific patterns that AI code generators commonly produce β including the less-obvious ones like insecure JWT options and disabled TLS verification.
Scan AI-generated code before it ships
AquilaX SAST includes rules specifically tuned for LLM-generated vulnerability patterns β catching what traditional SAST misses in AI-assisted development workflows.
Explore SAST β