PreBreachPreBreach
How it WorksMethodologyPricingBlog
Start Audit
HomeBlogVibe Coding Security: A Practical Guide to Securing Apps Built with Cursor, Bolt, and Lovable
Vibe Coding Security: A Practical Guide to Securing Apps Built with Cursor, Bolt, and Lovable

Vibe Coding Security: A Practical Guide to Securing Apps Built with Cursor, Bolt, and Lovable

2/20/2026
vibe codingai securitycursorboltlovablesecurity

Table of Contents

Vibe Coding Security: A Practical Guide to Securing Apps Built with Cursor, Bolt, and LovableWhat is Vibe Coding?Why AI-Generated Code Has Security GapsLLMs Learn from Stack Overflow PatternsNo Security ContextValidation Gets SkippedFramework Defaults Are Not EnoughThe 5 Most Common Vulnerability Categories in Vibe-Coded Apps1. Exposed Secrets and API KeysReal Examples.env file committed to the repo because .gitignore was not set upHow to CheckCheck for exposed secrets in source codeCheck git history for accidentally committed secretsCheck your built bundleHow to Fix2. Missing Authentication and Authorization ChecksReal ExamplesHow to CheckList all API routesCheck which ones import auth utilitiesFiles that do NOT import auth are likely unprotectedHow to Fix3. SQL Injection via Raw QueriesReal ExamplesHow to CheckFind raw SQL usageLook for string interpolation in SQL stringsHow to Fix4. CORS MisconfigurationReal ExamplesHow to CheckFind CORS configurationsHow to Fix5. Default Credentials and Debug ConfigurationsReal ExamplesHow to CheckFind debug configurationsFind hardcoded credentialsHow to FixAutomated Scanning as the SolutionA Security Workflow for Vibe Coders

Vibe Coding Security: A Practical Guide to Securing Apps Built with Cursor, Bolt, and Lovable

Vibe coding has changed how software gets built. You describe what you want in natural language, an AI generates the code, and you ship it. Tools like Cursor, Bolt, and Lovable have made it possible for a single developer to build and launch a full-stack application in a weekend.

But there is a problem nobody talks about enough: AI-generated code is consistently insecure by default.

This is not a hypothetical risk. We run PreBreach against dozens of vibe-coded applications every week, and the pattern is unmistakable. The same categories of vulnerabilities appear over and over, regardless of which AI tool generated the code.

This guide explains why this happens, what the most common vulnerabilities are, and exactly how to find and fix them in your own projects.


What is Vibe Coding?

Vibe coding is the practice of building software primarily through natural language prompts to AI coding assistants rather than writing code manually. The term was coined by Andrej Karpathy in early 2025 to describe a workflow where you:

  1. Describe what you want in plain English
  2. Let the AI generate the implementation
  3. Test it visually in the browser
  4. Iterate with more prompts until it works

The most popular vibe coding tools include:

  • Cursor - An AI-native code editor that uses Claude and GPT models to generate and edit code in an IDE context
  • Bolt - A browser-based tool that generates full-stack applications from a single prompt, deploying to a live URL
  • Lovable - Similar to Bolt, focused on generating complete web applications with a visual builder interface

These tools are genuinely impressive for productivity. The security problem is not that they exist. It is that the code they produce reflects the training data they learned from, and that training data is full of insecure patterns.


Why AI-Generated Code Has Security Gaps

Understanding why is important because it changes how you approach the problem. This is not about AI tools being "bad." It is a structural issue with how large language models generate code.

LLMs Learn from Stack Overflow Patterns

The training data for code-generating models includes millions of Stack Overflow answers, GitHub repositories, tutorials, and blog posts. The most common and highly-upvoted code patterns are often the simplest ones, and simple code tends to skip security considerations.

A Stack Overflow answer to "how do I query a database in Node.js" will show you string interpolation, not parameterized queries. The answer that gets the most upvotes is the one that is easiest to understand, not the one that is most secure.

No Security Context

When you prompt "build me a user dashboard with Supabase," the AI does not ask follow-up questions about your threat model. It does not consider:

  • Who should be able to access this data?
  • What happens if an unauthenticated user hits this endpoint?
  • Are there rate limits needed?
  • Should this API key be in the client bundle?

It generates the shortest path to a working feature. Security requirements are things you need to explicitly specify in your prompts, and most developers do not think to do so.

Validation Gets Skipped

AI-generated code often trusts all inputs. Form data flows directly into database queries. API request bodies are used without validation. URL parameters are embedded into responses without sanitization. The AI produces code that works for the happy path, but the happy path is not where attacks happen.

Framework Defaults Are Not Enough

Vibe coding tools tend to generate code using framework defaults. While modern frameworks like Next.js and SvelteKit have good security foundations, their defaults do not cover everything. CORS headers, Content Security Policy, authentication middleware, rate limiting, and input validation all require explicit configuration that AI tools rarely add.


The 5 Most Common Vulnerability Categories in Vibe-Coded Apps

Based on hundreds of scans we have run at PreBreach, these are the vulnerability categories that appear most frequently in applications built with AI coding tools. They are listed in rough order of severity and prevalence.


1. Exposed Secrets and API Keys

This is the single most common vulnerability we find. AI tools generate code that puts sensitive credentials directly in client-accessible files.

Real Examples

Hardcoded API keys in frontend code:

// Generated by AI in a client component
const stripe = new Stripe('sk_live_51ABC...real_key_here');

// Or in environment variables with the wrong prefix
// .env.local
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_51ABC...
NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY=eyJhbGci...
NEXT_PUBLIC_OPENAI_API_KEY=sk-proj-...

API keys committed to Git:

# .env file committed to the repo because .gitignore was not set up
STRIPE_SECRET_KEY=sk_live_...
DATABASE_URL=postgresql://user:password@host:5432/db
RESEND_API_KEY=re_...

How to Check

Search your codebase and git history:

# Check for exposed secrets in source code
grep -r "sk_live\|sk_test\|NEXT_PUBLIC.*SECRET\|NEXT_PUBLIC.*SERVICE_ROLE" src/
grep -r "password\|secret\|api_key" --include="*.env*" .

# Check git history for accidentally committed secrets
git log --all -p | grep -i "sk_live\|api_key\|secret_key" | head -20

# Check your built bundle
grep -r "sk_live\|eyJhbG" .next/static/

How to Fix

  1. Never prefix secret keys with NEXT_PUBLIC_ or VITE_. These prefixes exist specifically to expose variables to the browser.

  2. Use server-side API routes for sensitive operations. Move all third-party API calls to server-side code:

// app/api/create-checkout/route.ts (Server-side only)
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); // No NEXT_PUBLIC_ prefix

export async function POST(req: Request) {
  const { priceId } = await req.json();
  const session = await stripe.checkout.sessions.create({
    // ...
  });
  return Response.json({ url: session.url });
}
  1. Rotate any key that has ever been in client code or git history. Even if you remove it, it is in the git history forever (unless you rewrite history).

  2. Add a .gitignore check to your CI pipeline to ensure .env files are never committed.


2. Missing Authentication and Authorization Checks

AI tools build features but rarely build the gates around them. They create API routes that work when called correctly but do not verify who is calling them.

Real Examples

Unprotected API routes:

// app/api/users/[id]/route.ts
// Generated by AI - no auth check
export async function GET(
  req: Request,
  { params }: { params: { id: string } }
) {
  const user = await db.user.findUnique({
    where: { id: params.id },
  });
  return Response.json(user); // Returns any user's data to anyone
}

export async function DELETE(
  req: Request,
  { params }: { params: { id: string } }
) {
  await db.user.delete({ where: { id: params.id } });
  return Response.json({ success: true }); // Anyone can delete any user
}

IDOR (Insecure Direct Object Reference):

// User can access any invoice by changing the ID in the URL
// /api/invoices/123 -> /api/invoices/456 (another user's invoice)
export async function GET(req: Request, { params }: { params: { id: string } }) {
  const invoice = await db.invoice.findUnique({
    where: { id: params.id },
    // No check: does this invoice belong to the requesting user?
  });
  return Response.json(invoice);
}

How to Check

Review every API route and server action in your application:

# List all API routes
find src/app/api -name "route.ts" -o -name "route.js"

# Check which ones import auth utilities
grep -rL "getAuth\|getServerSession\|auth()\|currentUser\|getSession" src/app/api/
# Files that do NOT import auth are likely unprotected

How to Fix

Add authentication and authorization to every API route:

// app/api/users/[id]/route.ts - Secure version
import { auth } from '@/lib/auth'; // Your auth utility

export async function GET(
  req: Request,
  { params }: { params: { id: string } }
) {
  const session = await auth();
  if (!session?.user) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // Authorization: users can only access their own data
  if (session.user.id !== params.id) {
    return Response.json({ error: 'Forbidden' }, { status: 403 });
  }

  const user = await db.user.findUnique({
    where: { id: params.id },
  });
  return Response.json(user);
}

Create middleware that enforces auth on all API routes by default, then opt-out specific public routes:

// middleware.ts
import { auth } from '@/lib/auth';

const publicRoutes = ['/api/webhooks', '/api/health'];

export async function middleware(req: NextRequest) {
  if (req.nextUrl.pathname.startsWith('/api/') &&
      !publicRoutes.some(r => req.nextUrl.pathname.startsWith(r))) {
    const session = await auth();
    if (!session) {
      return Response.json({ error: 'Unauthorized' }, { status: 401 });
    }
  }
}

3. SQL Injection via Raw Queries

Modern ORMs like Prisma and Drizzle protect against SQL injection when used correctly. But AI-generated code frequently uses raw queries with string interpolation, especially for complex queries that the ORM does not handle elegantly.

Real Examples

String interpolation in raw SQL:

// AI-generated search endpoint
export async function GET(req: Request) {
  const { searchParams } = new URL(req.url);
  const query = searchParams.get('q');

  // VULNERABLE: Direct string interpolation
  const results = await prisma.$queryRawUnsafe(
    `SELECT * FROM products WHERE name LIKE '%${query}%'`
  );
  return Response.json(results);
}

An attacker sends: ?q=' OR '1'='1' --

Template literals in Drizzle raw SQL:

// VULNERABLE
const result = await db.execute(
  sql`SELECT * FROM users WHERE email = '${userInput}'`
);

How to Check

Search for raw query patterns:

# Find raw SQL usage
grep -rn "queryRaw\|queryRawUnsafe\|\$queryRaw\|execute.*sql\`" src/
grep -rn "db\.execute\|\.raw(" src/

# Look for string interpolation in SQL strings
grep -rn "\${.*}.*SELECT\|SELECT.*\${" src/
grep -rn "\${.*}.*INSERT\|INSERT.*\${" src/
grep -rn "\${.*}.*UPDATE\|UPDATE.*\${" src/

How to Fix

Always use parameterized queries:

// Prisma - use $queryRaw with tagged template (parameterized)
const results = await prisma.$queryRaw`
  SELECT * FROM products WHERE name LIKE ${`%${query}%`}
`;

// Drizzle - use the sql placeholder
import { sql } from 'drizzle-orm';
const result = await db.execute(
  sql`SELECT * FROM users WHERE email = ${userInput}`
);

// Better: use the ORM's query builder
const results = await prisma.product.findMany({
  where: {
    name: { contains: query },
  },
});

Note the subtle difference: $queryRaw with a tagged template literal is safe (Prisma parameterizes it). $queryRawUnsafe with a regular string is vulnerable. AI tools frequently confuse these.


4. CORS Misconfiguration

Cross-Origin Resource Sharing (CORS) controls which domains can make requests to your API. AI-generated code either ignores CORS entirely (relying on browser defaults) or sets it to allow everything.

Real Examples

Wildcard CORS in API routes:

// next.config.js generated by AI
module.exports = {
  async headers() {
    return [
      {
        source: '/api/:path*',
        headers: [
          { key: 'Access-Control-Allow-Origin', value: '*' },
          { key: 'Access-Control-Allow-Methods', value: '*' },
          { key: 'Access-Control-Allow-Headers', value: '*' },
          { key: 'Access-Control-Allow-Credentials', value: 'true' },
        ],
      },
    ];
  },
};

The combination of Access-Control-Allow-Origin: * and Access-Control-Allow-Credentials: true is especially dangerous. Browsers should reject this combination, but the intent reveals the developer does not understand CORS. They will likely "fix" it by dynamically reflecting the Origin header, which is equally insecure.

Dynamic origin reflection:

// Reflects any origin - equivalent to wildcard with credentials
export async function GET(req: Request) {
  const origin = req.headers.get('origin');
  return new Response(data, {
    headers: {
      'Access-Control-Allow-Origin': origin || '*', // Allows any site
      'Access-Control-Allow-Credentials': 'true',
    },
  });
}

How to Check

# Find CORS configurations
grep -rn "Access-Control-Allow-Origin\|cors\|CORS" src/ next.config*
grep -rn "allowedOrigins\|origin.*\*" src/

How to Fix

Explicitly whitelist only your own domains:

// lib/cors.ts
const allowedOrigins = [
  'https://yourdomain.com',
  'https://www.yourdomain.com',
  process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : '',
].filter(Boolean);

export function getCorsHeaders(req: Request) {
  const origin = req.headers.get('origin');
  const isAllowed = origin && allowedOrigins.includes(origin);

  return {
    'Access-Control-Allow-Origin': isAllowed ? origin : '',
    'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    'Access-Control-Allow-Credentials': 'true',
    'Access-Control-Max-Age': '86400',
  };
}

If your API is only consumed by your own frontend (same origin), you do not need CORS headers at all. Remove them entirely.


5. Default Credentials and Debug Configurations

AI tools scaffold applications with default configurations that are intended for development but ship to production unchanged.

Real Examples

Debug mode in production:

// next.config.js
module.exports = {
  // AI left debug logging enabled
  logging: {
    fetches: { fullUrl: true },
  },
  // Source maps exposed in production
  productionBrowserSourceMaps: true,
};

Default admin credentials:

// seed.ts generated by AI - but these credentials end up in production
await db.user.create({
  data: {
    email: 'admin@example.com',
    password: await hash('admin123'),
    role: 'admin',
  },
});

Verbose error messages in production:

// API route with full error stack in response
export async function POST(req: Request) {
  try {
    // ...
  } catch (error) {
    return Response.json({
      error: error.message,
      stack: error.stack,     // Exposes internal file paths
      query: rawSqlQuery,     // Exposes database schema
    }, { status: 500 });
  }
}

How to Check

# Find debug configurations
grep -rn "debug.*true\|verbose.*true\|sourceMaps.*true" next.config* src/
grep -rn "stack.*error\|error\.stack\|error\.message" src/app/api/

# Find hardcoded credentials
grep -rn "admin123\|password123\|default.*password\|test.*password" src/
grep -rn "admin@example\|test@test" src/ --include="*.ts"

How to Fix

  1. Use environment-based configuration:
// Return safe error responses in production
catch (error) {
  console.error('API Error:', error); // Log internally
  return Response.json(
    { error: process.env.NODE_ENV === 'development'
        ? error.message
        : 'Internal server error' },
    { status: 500 }
  );
}
  1. Remove seed data credentials before deploying. Or better, use environment variables for seed data.

  2. Disable source maps in production (this is the Next.js default, but AI tools sometimes override it).

  3. Add security headers:

// next.config.js
const securityHeaders = [
  { key: 'X-Frame-Options', value: 'DENY' },
  { key: 'X-Content-Type-Options', value: 'nosniff' },
  { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
  { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
];

Automated Scanning as the Solution

Here is the uncomfortable truth about vibe coding security: you cannot rely on the AI to secure its own output, and manual code review does not scale when you are shipping features at vibe-coding speed.

If you are generating hundreds of lines of code per hour with Cursor or Bolt, you are not going to manually audit every API route, every database query, and every environment variable. You need automated tools that do this for you.

This is the problem PreBreach was built to solve. Our platform runs 8 AI agents powered by Claude Opus that understand the specific technology stacks used in vibe-coded applications: Next.js, Supabase, Firebase, Vercel, Clerk. We have 24 custom scanning templates designed for these modern stacks, covering the exact vulnerability categories described in this article.

Here is what a scan covers:

  • Secret exposure - Scanning client bundles, API responses, and git history for leaked credentials
  • Authentication gaps - Testing every API endpoint for missing or bypassable auth checks
  • Authorization failures - Attempting to access other users' data through IDOR and privilege escalation
  • Injection vulnerabilities - Testing all input points for SQL injection, XSS, and command injection
  • Configuration issues - Checking CORS, security headers, debug modes, and default credentials

Each finding gets a CVSS v4.0 severity score, a detailed explanation of the risk, and specific remediation steps for your stack. Our multi-model validation (GPT cross-checking Claude's findings) reduces false positives so you are not wasting time on non-issues.


A Security Workflow for Vibe Coders

You do not need to become a security expert to ship secure vibe-coded applications. You need a process:

  1. When prompting AI tools, include security requirements. Instead of "build a user dashboard," try "build a user dashboard with authentication checks on all API routes, parameterized database queries, and no client-exposed secrets."

  2. Run a grep audit before every deploy. Search for the patterns listed in this article. It takes two minutes and catches the obvious issues.

  3. Use automated security scanning. Run PreBreach against your staging environment before going live. Plans start at $29/month and give you complete assessments with prioritized findings.

  4. Fix the critical issues first. Exposed secrets and missing auth checks are critical. CORS and debug configs are important but less urgent. Use severity scores to prioritize.

  5. Re-scan after major changes. Every time you generate a significant amount of new code, run another scan. New features mean new attack surface.

Vibe coding is not going away, and it should not. Building faster is good. But shipping insecure code at high velocity is a liability. The developers who combine AI-powered building with AI-powered security testing are the ones who will build sustainably.

Your AI coding tool writes the code. Make sure something is checking its work.

Related Articles

We Analyzed Apps Built with Lovable and Bolt — Here Are the Security Vulnerabilities We Found

We Analyzed Apps Built with Lovable and Bolt — Here Are the Security Vulnerabilities We Found

OWASP Top 10 in AI-Generated Code: The Vulnerabilities Your AI Keeps Writing

OWASP Top 10 in AI-Generated Code: The Vulnerabilities Your AI Keeps Writing

The 7 Supabase RLS Mistakes That Expose Your Entire Database

The 7 Supabase RLS Mistakes That Expose Your Entire Database

Table of Contents

Vibe Coding Security: A Practical Guide to Securing Apps Built with Cursor, Bolt, and LovableWhat is Vibe Coding?Why AI-Generated Code Has Security GapsLLMs Learn from Stack Overflow PatternsNo Security ContextValidation Gets SkippedFramework Defaults Are Not EnoughThe 5 Most Common Vulnerability Categories in Vibe-Coded Apps1. Exposed Secrets and API KeysReal Examples.env file committed to the repo because .gitignore was not set upHow to CheckCheck for exposed secrets in source codeCheck git history for accidentally committed secretsCheck your built bundleHow to Fix2. Missing Authentication and Authorization ChecksReal ExamplesHow to CheckList all API routesCheck which ones import auth utilitiesFiles that do NOT import auth are likely unprotectedHow to Fix3. SQL Injection via Raw QueriesReal ExamplesHow to CheckFind raw SQL usageLook for string interpolation in SQL stringsHow to Fix4. CORS MisconfigurationReal ExamplesHow to CheckFind CORS configurationsHow to Fix5. Default Credentials and Debug ConfigurationsReal ExamplesHow to CheckFind debug configurationsFind hardcoded credentialsHow to FixAutomated Scanning as the SolutionA Security Workflow for Vibe Coders

Ready to get started?

Join our team of 5,000+ users who are already transforming their workflow with PreBreach.

5,000+ active users
Get PreBreach Pro

Plans starting from $29/month

PreBreach

Secure your vibe coding. Built for the new generation of AI-assisted developers.

All Systems Operational

Product

  • Pricing
  • Sample Report
  • Documentation

Resources

  • Blog
  • Contact

Connect

  • Twitter / X

© 2026 PreBreach Security. All rights reserved.

Privacy PolicyTerms of Service