TL;DR: AI coding tools generate functional code but routinely introduce serious security vulnerabilities — hardcoded secrets, SQL injection, missing input validation, exposed .env files, and dangerously open CORS settings. This article shows you exactly what those look like in AI-generated code, how to fix each one, and a pre-ship checklist to run before every deployment. The five minutes you spend here could save you from a catastrophic breach.

Why Security Is the Competitive Moat No One Talks About

Walk through any popular AI coding community and you'll find endless guides on how to build things with AI. Very few of them spend serious time on security. Most treat it as an afterthought — "you should probably add auth at some point" — and move on.

That's a problem, because AI-generated code has a distinct security profile. It's not random badness. It's a specific set of repeatable mistakes that appear across thousands of AI-generated projects because AI models were trained on the same patterns, the same Stack Overflow answers, the same tutorial code that optimized for "easy to understand" over "secure to ship."

The vibe coding community is building real things. Contact forms that write to databases. APIs that accept user input. Apps with payment flows. Authentication systems. These are production-grade surfaces being built by people who are excellent at getting AI to build what they need — but who haven't had the chance to learn the security layer that traditional developers pick up over years.

This article is that layer. Not theory, not CISSP certification material — just the five things AI gets wrong most consistently, shown with real code examples, with clear fixes for each one.

🔴 Core Truth

AI writes plausible-looking code. It does not write secure code by default. The responsibility for security sits with you, the builder — not the AI tool. Understanding these patterns is non-negotiable before you ship anything that touches real users or real data.

Real Scenario: You Asked Claude to Build a Contact Form

Here's a real-world scenario that plays out constantly in the vibe coding world. You asked Claude to build you a contact form that saves submissions to a PostgreSQL database so you can review them later. You gave it a simple prompt:

Build me a contact form with Node.js and Express that accepts name, email, and message, saves the submission to a PostgreSQL database, and sends me a notification email via Mailgun. I want to see all submissions in an admin panel at /admin.

Claude will generate something that works. You can run it locally, fill out the form, see the data appear in the database, get the email notification. It feels complete. But inside that working, functional code, there are likely four to five serious security vulnerabilities that Claude introduced without explanation.

Let's go through each one — with the exact code pattern Claude might produce, why it's dangerous, and how to fix it.

The 5 Security Mistakes AI Makes Most Often

1

Hardcoded Secrets

API keys and passwords written directly into source code

When you ask Claude to connect your app to a database or an email service, it will often write code that looks like this:

❌ What AI Often Generates
// server.js
const { Pool } = require('pg');

const pool = new Pool({
  host: 'db.myapp.com',
  database: 'myapp_production',
  user: 'admin',
  password: 'MySuper$ecretPassword123',  // ← DANGER
  port: 5432
});

const mailgun = require('mailgun-js')({
  apiKey: 'key-abc123def456ghi789jkl012',  // ← DANGER
  domain: 'mg.myapp.com'
});

This code works perfectly. And if you push it to a public GitHub repository, automated bots will find those credentials within minutes. GitHub has bots scanning every public commit for credential patterns. So does every major threat intelligence service. Your database and your Mailgun account will be compromised before you've even finished your coffee.

✅ The Fix: Environment Variables
// server.js
require('dotenv').config();
const { Pool } = require('pg');

const pool = new Pool({
  host: process.env.DB_HOST,
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,  // ← reads from .env
  port: process.env.DB_PORT || 5432
});

const mailgun = require('mailgun-js')({
  apiKey: process.env.MAILGUN_API_KEY,  // ← reads from .env
  domain: process.env.MAILGUN_DOMAIN
});

Your .env file (which stays on your machine or server, never in Git) looks like:

# .env
DB_HOST=db.myapp.com
DB_NAME=myapp_production
DB_USER=admin
DB_PASSWORD=MySuper$ecretPassword123
MAILGUN_API_KEY=key-abc123def456ghi789jkl012
MAILGUN_DOMAIN=mg.myapp.com

Install the dotenv package with npm install dotenv and call require('dotenv').config() at the top of your entry file. Variables are now loaded from .env at runtime — they never touch your source code.

2

SQL Injection

Building database queries with string concatenation

SQL injection has been in the OWASP Top 10 for over two decades. AI still generates it regularly because string-concatenated queries are simple and readable — exactly the kind of pattern that appears in tutorials and examples that AI was trained on.

❌ What AI Often Generates
// routes/admin.js
app.get('/admin/search', async (req, res) => {
  const { email } = req.query;
  
  // Building query with string concatenation — SQL INJECTION RISK
  const query = `SELECT * FROM submissions WHERE email = '${email}'`;
  
  const result = await pool.query(query);
  res.json(result.rows);
});

If someone visits /admin/search?email=' OR '1'='1, the resulting query becomes SELECT * FROM submissions WHERE email = '' OR '1'='1' — which returns every row in the table. A more malicious payload could drop your entire database. The attack can happen through any URL parameter, form field, or header that gets inserted into a SQL string.

✅ The Fix: Parameterized Queries
// routes/admin.js
app.get('/admin/search', async (req, res) => {
  const { email } = req.query;
  
  // Parameterized query — user input is NEVER executed as SQL
  const query = 'SELECT * FROM submissions WHERE email = $1';
  
  const result = await pool.query(query, [email]);
  res.json(result.rows);
});

The $1 is a placeholder. The database driver sends the SQL structure and the user data as completely separate items to the database engine. The engine treats $1's value as pure data — never as executable SQL. It's impossible to inject SQL through a parameterized query, no matter what the user submits.

3

Missing Input Validation

Trusting user input and storing it directly

Your contact form accepts a message field. Claude's generated handler probably takes that message and stores it directly in the database and sends it in an email. There's no check on what the message actually contains.

❌ What AI Often Generates
app.post('/contact', async (req, res) => {
  const { name, email, message } = req.body;
  
  // Storing raw, unvalidated user input
  await pool.query(
    'INSERT INTO submissions (name, email, message) VALUES ($1, $2, $3)',
    [name, email, message]
  );
  
  res.json({ success: true });
});

Problems here: There's no limit on message length (someone could submit a 10MB message, repeatedly, crashing your server). There's no email format validation (so garbage data gets stored). There's no sanitization of HTML in the message field (which matters if you ever display submissions in a browser — this is how XSS attacks work). And there's nothing preventing someone from submitting this form 10,000 times automatically.

✅ The Fix: Validate and Sanitize
const { body, validationResult } = require('express-validator');

app.post('/contact',
  // Validate before your handler runs
  body('name').trim().isLength({ min: 1, max: 100 }).escape(),
  body('email').isEmail().normalizeEmail(),
  body('message').trim().isLength({ min: 1, max: 2000 }).escape(),
  
  async (req, res) => {
    // Check for validation errors first
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    
    const { name, email, message } = req.body;
    
    await pool.query(
      'INSERT INTO submissions (name, email, message) VALUES ($1, $2, $3)',
      [name, email, message]
    );
    
    res.json({ success: true });
  }
);

Install with npm install express-validator. The .escape() call converts HTML characters to entities, preventing XSS. The length limits prevent database spam. The email validator ensures you're storing real email addresses. This is the minimum validation baseline for any public-facing form.

4

Exposed .env Files

Not adding .env to .gitignore before the first commit

This is the most common catastrophic mistake for vibe coders specifically, because it combines two concepts (environment variables and Git) that AI often teaches separately without connecting the critical safety rule between them.

The scenario: Claude tells you to use a .env file. You follow the instructions, create the file, add your real credentials. Then you initialize a Git repo and push to GitHub. If you didn't add .env to .gitignore before that first git add ., your secrets are now public.

❌ The Dangerous Commit
# Terminal — missing the .gitignore step
git init
git add .        # ← This adds .env if .gitignore doesn't exist yet
git commit -m "initial commit"
git push origin main  # ← .env is now on GitHub forever

Even if you delete the file in a later commit, Git history is permanent. Anyone who cloned your repo or looked at your commit history still has those credentials. You must regenerate every secret that was ever exposed.

✅ The Fix: .gitignore First, Always
# .gitignore — create this BEFORE your first git add
.env
.env.local
.env.production
.env.*.local
node_modules/
*.log
# Safe commit sequence
git init
# Create .gitignore first ↑
git add .gitignore
git commit -m "add gitignore"
git add .    # .env is now excluded
git commit -m "initial commit"
git push origin main
⚠️ If You Already Pushed .env

Stop. Go to every service that credential belongs to and rotate the secret immediately — generate a new API key, change the password, revoke the token. Don't wait. Automated bots scan GitHub continuously. Deleting the file from Git history using git filter-repo is good practice but treat the exposed credential as already compromised.

5

CORS Misconfiguration

Access-Control-Allow-Origin: * in production

CORS (Cross-Origin Resource Sharing) is the browser security feature that controls which domains can make API requests to your server. When you ask Claude to "fix the CORS error" during development, it commonly generates the nuclear option: allow every origin.

❌ What AI Often Generates
const cors = require('cors');

// Allows ANY website to call your API — dangerous in production
app.use(cors());
// Which is equivalent to:
app.use(cors({ origin: '*' }));

In development this is fine — your localhost:3000 front-end needs to talk to localhost:3001 back-end. But in production, Access-Control-Allow-Origin: * means any website in the world can make requests to your API in the context of a logged-in user's browser. This enables cross-site request forgery (CSRF) attacks and allows malicious sites to access your users' data.

✅ The Fix: Explicit Origin Allowlist
const cors = require('cors');

const allowedOrigins = process.env.NODE_ENV === 'production'
  ? ['https://myapp.com', 'https://www.myapp.com']
  : ['http://localhost:3000', 'http://localhost:5173'];

app.use(cors({
  origin: (origin, callback) => {
    // Allow requests with no origin (like curl or Postman)
    if (!origin) return callback(null, true);
    if (allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true  // Required if you use cookies or Authorization headers
}));

This explicitly allowlists only your real domain in production. In development, it allows localhost origins for your front-end dev server. The credentials: true option is needed if your app uses cookies for authentication or sends Authorization headers — and it requires an explicit origin (not *), which is another reason the wildcard breaks real-world auth flows.

Pre-Ship Security Checklist

Run through this before every deployment. It takes five minutes and covers the most common issues in AI-generated code.

🛡️ Security Checklist — Run Before Every Deploy

Secrets & Credentials

  • All API keys, passwords, and tokens are in .env (not in source code)
  • .env is listed in .gitignore before any git add
  • Run: git log --all --full-history -- .env — should return nothing
  • Run: grep -r "password\|apiKey\|secret\|token" --include="*.js" . — review any hits
  • GitHub Secret Scanning is enabled on your repository

Database & Input

  • All database queries use parameterized queries (no string concatenation)
  • All form inputs are validated (type, length, format) before processing
  • User-supplied data that gets rendered in HTML is escaped
  • File uploads (if any) check file type server-side, not just client-side

API & CORS

  • CORS is configured with an explicit allowlist, not origin: '*'
  • Admin endpoints require authentication — no unprotected /admin routes
  • Rate limiting is in place for form submissions and auth endpoints
  • HTTP-only, Secure flags are set on any authentication cookies

Dependencies & Config

  • Run npm audit — review and address any high/critical vulnerabilities
  • Error responses don't expose stack traces or internal details to the browser
  • Node.js and npm versions are up to date
  • HTTPS is enforced (no plain HTTP in production)

Security Tools Every Vibe Coder Should Know

npm audit

Run npm audit in your project directory. It checks every package in your node_modules against the npm advisory database and lists known vulnerabilities by severity. Run npm audit fix to automatically update packages to patched versions (for non-breaking upgrades). This is your first line of defense against supply-chain attacks through your dependencies.

# Run in your project directory
npm audit

# Auto-fix what it can without breaking changes
npm audit fix

# See the full audit report
npm audit --json

GitHub Secret Scanning

For public repositories, GitHub Secret Scanning is free and automatic. It scans every commit for known credential patterns (AWS keys, Stripe keys, GitHub tokens, etc.) and alerts you — and often the affected service provider — when it finds one. For private repos, it's included in GitHub Advanced Security. Enable it in your repo Settings → Security → Secret scanning. This won't save you if a secret was already exposed publicly, but it catches future accidents immediately.

OWASP ZAP (Advanced)

OWASP ZAP (Zed Attack Proxy) is a free, open-source web security scanner that can automatically test your running app for common vulnerabilities. It's more complex to set up, but the automated scan will catch things like missing security headers, XSS opportunities, and open admin panels that you might miss manually. Run it against a staging environment before a major launch.

How to Use Claude Code for a Security Review

Here's a practical workflow for using AI to audit AI-generated code. The key is giving Claude Code enough context and asking the right questions.

Please do a security review of this Express.js backend. Focus on the OWASP Top 10 vulnerabilities, specifically: SQL injection, hardcoded credentials, missing authentication, CORS misconfiguration, and improper error handling. For each vulnerability you find, show me the specific line, explain why it's a risk, and provide the fixed code.

Then paste in your full server file. Claude Code will typically find:

  • Any remaining hardcoded values (it's good at pattern-matching these)
  • String-concatenated SQL queries
  • Unprotected routes that should require auth
  • Missing rate limiting on sensitive endpoints
  • Error handlers that expose stack traces in responses
💡 Cursor / Windsurf Tip

In Cursor, open your main server file and press Cmd+K (or Ctrl+K on Windows). Type: "Security review — check for OWASP Top 10 issues." Cursor will inline the security findings and suggested fixes directly in your editor. You can accept individual fixes without rewriting the entire file.

What to Ask Claude Code Specifically

Generic "is this secure?" prompts get generic answers. These specific prompts get actionable results:

  • "Are there any places where user input is directly concatenated into a SQL query?"
  • "Is there any string that looks like an API key, password, or token hardcoded in this file?"
  • "Which routes in this Express app don't have authentication middleware?"
  • "What could a malicious user submit to the contact form to cause problems?"
  • "Does this error handler expose internal details to the browser?"

When to Hire a Real Security Professional

This article gets you the fundamentals — and for personal projects, side businesses, and early-stage apps, the basics above cover the vast majority of real-world risk. But there are situations where you need a professional security review.

🏦 Hire a Pro When:

Handling payment card data (PCI-DSS required). Storing protected health info (HIPAA). Managing auth for 10,000+ users. Any enterprise B2B contract requiring a security audit. Financial services or regulated industries.

✅ Basics Are Enough For:

Personal projects. Early-stage MVPs. Simple SaaS apps under ~$10K MRR. Portfolio sites. Internal tools. Anything not handling sensitive personal or financial data at scale.

A professional penetration tester or security engineer will do things this article can't: test your live application's behavior under attack conditions, check your server configuration, audit your infrastructure, and review your authentication flow end-to-end. Expect to pay $2,000–$15,000 for a focused security review for a small app. It's expensive — but a single breach can cost orders of magnitude more.

The honest guidance: the mistakes covered in this article are free to fix and affect 90% of AI-generated apps. Fix them first. Hire a professional when you have paying customers whose data you're responsible for, or when a business relationship requires it.

What to Learn Next

Security doesn't live in isolation — it intersects with your API design, your database schema, and your deployment practices. Here's where to go deeper:

FAQ

Not automatically. AI tools like Claude and ChatGPT produce plausible-looking, functional code — but they optimize for working, not secure. They are trained on the entire internet, which includes millions of insecure code examples. AI routinely hardcodes secrets, skips input validation, and uses vulnerable query patterns. You must review AI-generated code for security issues before shipping to production. The five issues in this article cover the most common patterns.

Hardcoded secrets are the most common and most immediately damaging risk. When an API key, database password, or JWT secret is committed to a public GitHub repository, automated scanners find it within minutes. Attackers use those credentials immediately — before you've even noticed. The fix is simple: store all secrets in environment variables (.env files) and add .env to .gitignore before your very first git add command.

SQL injection is an attack where a malicious user inserts SQL commands into your input fields, which then execute as actual database queries — potentially reading, modifying, or deleting your entire database. AI produces it because string-concatenated queries are the simplest, most readable way to write database queries, and AI optimizes for readable tutorial-style examples. The fix is parameterized queries (prepared statements), which treat user input as data, never as executable code. Every major database library supports them — there's no excuse not to use them.

Start with npm audit in your project directory — it checks your dependencies against a known vulnerability database and shows you what to update. For secret scanning, enable GitHub Secret Scanning on your repository (free for public repos). For a deeper code review, ask Claude Code to audit your codebase: paste your key server files and ask "What security vulnerabilities do you see in this code? Focus on OWASP Top 10 issues and show me the specific lines." Then run through the pre-ship checklist in this article before every deployment.

Hire a security professional when you're handling payment card data (requires PCI-DSS compliance), storing protected health information (requires HIPAA compliance), managing authentication for thousands of users, handling sensitive personal data at scale, or when a business contract explicitly requires a security audit or penetration test. For personal projects and early-stage apps, the five fundamentals in this article get you 80% of the protection you need for free. The basics first — professionals when stakes are high.