Code Quality & Security March 23, 2026 · 6 min read

Common Security Holes in Vibe-Coded Apps (and How to Fix Them)

Common Security Holes in Vibe-Coded Apps (and How to Fix Them)

Building apps with AI assistance is incredible - you can ship features at lightning speed and prototype ideas faster than ever. But here's the thing: AI coding assistants are amazing at generating functional code, but they're not security experts. They'll happily create a working login system without implementing proper rate limiting, or build an API that's wide open to SQL injection.

As vibe coders, we need to level up our security game. Let's dive into the most common security holes I see in AI-generated apps and how to fix them before they bite you in production.

1. Exposed API Keys and Secrets

The Problem: AI assistants often generate code with hardcoded API keys, database URLs, and other secrets right in your source code. It's the path of least resistance for getting things working quickly.

// Don't do this (but AI might suggest it)
const openaiClient = new OpenAI({
  apiKey: 'sk-1234567890abcdef...'
});

const dbUrl = 'postgresql://user:password@host:5432/database';

The Fix: Always use environment variables and proper secrets management.

// Much better
const openaiClient = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY
});

const dbUrl = process.env.DATABASE_URL;

Pro tip: Add a .env.example file to your repo with dummy values, and never commit your actual .env file. Use a secrets manager like AWS Secrets Manager or Vercel's environment variables for production.

2. Missing Input Validation and Sanitization

AI loves to create clean, happy-path code. It assumes users will behave nicely and send properly formatted data. Spoiler alert: they won't.

The Problem: No validation on user inputs leads to injection attacks, crashes, and data corruption.

// Dangerous - no validation
app.post('/api/users', (req, res) => {
  const { name, email, age } = req.body;
  const user = new User({ name, email, age });
  user.save();
});

The Fix: Always validate and sanitize inputs.

// Better - with validation
const { z } = require('zod');

const userSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
  age: z.number().int().min(13).max(120)
});

app.post('/api/users', (req, res) => {
  try {
    const validatedData = userSchema.parse(req.body);
    const user = new User(validatedData);
    user.save();
  } catch (error) {
    return res.status(400).json({ error: 'Invalid input' });
  }
});

3. Weak Authentication and Session Management

AI assistants often implement basic auth systems that work for demos but fall apart under real-world conditions.

The Problem: No rate limiting, weak password requirements, insecure session handling.

// Problematic login endpoint
app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const user = await User.findOne({ email });
  
  if (user && user.password === password) {
    req.session.userId = user.id;
    res.json({ success: true });
  } else {
    res.status(401).json({ error: 'Invalid credentials' });
  }
});

The Fix: Implement proper auth with hashing, rate limiting, and secure sessions.

const bcrypt = require('bcrypt');
const rateLimit = require('express-rate-limit');

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // 5 attempts per window
  message: 'Too many login attempts'
});

app.post('/login', loginLimiter, async (req, res) => {
  const { email, password } = req.body;
  const user = await User.findOne({ email });
  
  if (user && await bcrypt.compare(password, user.hashedPassword)) {
    req.session.userId = user.id;
    res.json({ success: true });
  } else {
    res.status(401).json({ error: 'Invalid credentials' });
  }
});

4. Inadequate CORS Configuration

AI often suggests wide-open CORS policies to "fix" those annoying browser errors during development.

The Problem: Overly permissive CORS allows any website to make requests to your API.

// Too permissive
app.use(cors({ origin: '*' }));

The Fix: Configure CORS properly for your specific domains.

// Production-ready CORS
const corsOptions = {
  origin: process.env.NODE_ENV === 'production' 
    ? ['https://yourapp.com', 'https://www.yourapp.com']
    : ['http://localhost:3000', 'http://localhost:5173'],
  credentials: true,
  optionsSuccessStatus: 200
};

app.use(cors(corsOptions));

5. Missing Authorization Checks

Authentication ("who are you?") and authorization ("what can you do?") are different things. AI often handles the first but forgets the second.

The Problem: Users can access or modify data they shouldn't be able to.

// Missing authorization
app.delete('/api/posts/:id', authenticateUser, async (req, res) => {
  await Post.findByIdAndDelete(req.params.id);
  res.json({ success: true });
});

The Fix: Always check if the user has permission for the specific action.

// With proper authorization
app.delete('/api/posts/:id', authenticateUser, async (req, res) => {
  const post = await Post.findById(req.params.id);
  
  if (!post) {
    return res.status(404).json({ error: 'Post not found' });
  }
  
  // Check if user owns the post or is admin
  if (post.authorId.toString() !== req.user.id && !req.user.isAdmin) {
    return res.status(403).json({ error: 'Not authorized' });
  }
  
  await post.delete();
  res.json({ success: true });
});

6. Insecure File Uploads

File uploads are a classic attack vector, but AI assistants often implement them without proper security measures.

The Problem: No file type validation, size limits, or malware scanning.

The Fix: Implement comprehensive file upload security.

const multer = require('multer');
const path = require('path');

const storage = multer.diskStorage({
  destination: './uploads/',
  filename: (req, file, cb) => {
    // Generate unique filename
    cb(null, Date.now() + '-' + Math.round(Math.random() * 1E9) + path.extname(file.originalname));
  }
});

const upload = multer({
  storage,
  limits: {
    fileSize: 5 * 1024 * 1024, // 5MB limit
  },
  fileFilter: (req, file, cb) => {
    // Only allow specific file types
    const allowedTypes = /jpeg|jpg|png|gif/;
    const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
    const mimetype = allowedTypes.test(file.mimetype);
    
    if (extname && mimetype) {
      return cb(null, true);
    } else {
      cb(new Error('Only image files are allowed'));
    }
  }
});

7. Lack of HTTPS and Security Headers

AI-generated deployment configs often skip essential security configurations.

The Fix: Always use HTTPS and implement security headers.

const helmet = require('helmet');

// Add security headers
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      scriptSrc: ["'self'"],
      imgSrc: ["'self'", "data:", "https:"],
    },
  },
}));

// Force HTTPS in production
if (process.env.NODE_ENV === 'production') {
  app.use((req, res, next) => {
    if (req.header('x-forwarded-proto') !== 'https') {
      res.redirect(`https://${req.header('host')}${req.url}`);
    } else {
      next();
    }
  });
}

Building Security into Your Vibe Coding Workflow

Here's how to make security a natural part of your AI-assisted development process:

  1. Create security-focused prompts: Instead of just asking "build me a login system," ask "build me a secure login system with rate limiting, password hashing, and proper session management."

  2. Use security-focused libraries: Integrate tools like Helmet, bcrypt, and Zod from the start.

  3. Security checklist for every feature: Before deploying, run through authentication, authorization, input validation, and error handling.

  4. Automated security scanning: Use tools like npm audit, Snyk, or GitHub's security advisories in your CI/CD pipeline.

The Bottom Line

AI coding assistants are incredible productivity multipliers, but they're not security experts. They'll generate working code that gets you 80% of the way there, but you need to handle the security details.

The good news? Once you know these common patterns, fixing them becomes second nature. Start implementing these security practices in your next vibe-coded project, and you'll ship apps that are both fast to build and secure to run.

Remember: it's much easier to build security in from the start than to bolt it on later. Your users (and your sleep schedule) will thank you for it.

Alex Hackney

Alex Hackney

DeployMyVibe

Ready to deploy?

Stop reading about it. Start shipping.

View Pricing