5 Security Fixes Every Vibe Coder Should Know
You shipped your app in a weekend with Cursor, Lovable, or Bolt. It works. Users are signing up. But there is a good chance it can be hacked in under 5 minutes.
We have scanned thousands of vibe-coded apps. The same 5 vulnerabilities show up in over 70% of them. The good news: every one of them can be fixed in under 10 minutes with a code snippet you paste into your editor.
1. Move Your Secrets Out of the Client Bundle
This is the single most common critical vulnerability in AI-built apps. When you tell Cursor or Lovable "connect to Supabase," the AI often puts your service_role key directly in client-side code. Anyone can open DevTools and find it.
The problem: Your Supabase service role key, Stripe secret key, or OpenAI API key is visible in the JavaScript bundle that gets sent to every visitor's browser.
Before (exposed in client code):
// src/lib/supabase.ts — BAD
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY! // Anyone can see this
)
After (server-side only):
// src/app/api/data/route.ts — SAFE
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY! // No NEXT_PUBLIC_ prefix = server only
)
export async function GET() {
const { data } = await supabase.from('items').select('*')
return Response.json(data)
}
Rule of thumb: If an environment variable has NEXT_PUBLIC_ in the name, it is visible to everyone. Only your anon key should ever be public. Everything else goes server-side.
2. Add Row Level Security (RLS) to Every Table
Supabase ships with RLS disabled by default. AI tools almost never enable it. That means anyone with your anon key (which is public) can read, update, or delete every row in every table.
The problem: Your database tables have no access policies, so the public anon key grants full read/write access to all data.
Enable RLS and add a basic policy:
-- Enable RLS on every table
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- Users can only see their own data
CREATE POLICY "Users see own data" ON profiles
FOR SELECT USING (auth.uid() = user_id);
-- Users can only update their own data
CREATE POLICY "Users update own data" ON profiles
FOR UPDATE USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
Critical detail: Every UPDATE policy needs both USING and WITH CHECK. Without WITH CHECK, a user could change their role column from "user" to "admin" and you would never know.
3. Enable Content-Security-Policy Headers
Without CSP, an attacker who finds any XSS vulnerability can inject scripts that steal session tokens, redirect users to phishing pages, or exfiltrate data to their own server.
The problem: Your app has no Content-Security-Policy header, so injected scripts can load from anywhere.
Add CSP in next.config.ts:
// next.config.ts
const nextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self' https://*.supabase.co",
].join('; '),
},
],
},
]
},
}
export default nextConfig
Start with a permissive policy and tighten it. A loose CSP is still infinitely better than no CSP.
4. Add Rate Limiting to Login and API Routes
AI-generated auth flows almost never include rate limiting. That means an attacker can try thousands of passwords per minute against your login endpoint.
The problem: Your /api/auth/login endpoint accepts unlimited requests with no throttling.
Add rate limiting with Upstash (works on Vercel):
// src/app/api/auth/login/route.ts
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
import { headers } from 'next/headers'
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(5, '60 s'),
})
export async function POST(req: Request) {
const headersList = await headers()
const ip = headersList.get('x-forwarded-for') ?? '127.0.0.1'
const { success } = await ratelimit.limit(ip)
if (!success) {
return Response.json(
{ error: 'Too many attempts. Try again in 60 seconds.' },
{ status: 429 }
)
}
// ... your login logic
}
5 attempts per minute is a good starting point for login. For general API routes, 60 requests per minute works for most apps.
5. Lock Down CORS to Your Own Domain
AI tools often set CORS to allow all origins (*) because it "just works." This means any website on the internet can make authenticated requests to your API on behalf of your users.
The problem: Your API responds with Access-Control-Allow-Origin: *, allowing cross-origin requests from any domain.
Restrict CORS to your domain:
// src/app/api/data/route.ts
const ALLOWED_ORIGINS = [
'https://yourapp.com',
'https://www.yourapp.com',
]
export async function GET(req: Request) {
const origin = req.headers.get('origin') ?? ''
const isAllowed = ALLOWED_ORIGINS.includes(origin)
const data = await fetchData()
return Response.json(data, {
headers: {
'Access-Control-Allow-Origin': isAllowed ? origin : '',
'Access-Control-Allow-Methods': 'GET, POST',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
})
}
If you are building a public API that other sites intentionally consume, CORS * is fine. For everything else, restrict it.
How Bad Is Your App?
These 5 fixes cover the most common vulnerabilities, but there are dozens more that AI tools introduce. Exposed admin routes, verbose error messages, open database dashboards, missing session validation.
The fastest way to find out what is actually exploitable in your app is to scan it.
Scan your app free
Paste a URL, get a letter grade and Cursor-ready fixes in 3 minutes. No signup required.
Start Free Scan