The 15-minute audit before you share that URL.
Seven security checks that cover the bugs behind every major vibe-coding breach. Each step takes minutes, works with any AI coding tool, and tells you exactly what to run.
AI assistants put API keys where they don't belong. These checks find them before someone else does.
Open your deployed app, open your browser's developer tools (right-click anywhere → Inspect → Sources tab), and search all files for secret key patterns. Or paste the command below into your terminal.
Variables prefixed with NEXT_PUBLIC_ (Next.js), VITE_ (Vite), or EXPO_PUBLIC_ (Expo) are bundled into your client JavaScript — anyone viewing source can read them. That's fine for publishable keys. It's catastrophic for secret keys.
A login page is not authentication. These checks verify your API actually rejects unauthorized requests.
Log in, open your browser's DevTools Network tab, find any API request. Right-click → Copy as cURL. Strip the Authorization header and re-run it. If you still get data back, your auth is cosmetic — the login page exists, but the API behind it has no real protection.
Create two accounts. As User A, grab an ID from any API call (watch the Network tab). Log in as User B and request User A's resource by pasting that ID into the URL. If it returns data, you have an IDOR (Insecure Direct Object Reference) — the endpoint looks up records by ID without checking who owns them.
If you're on Supabase or Firebase, your database might be wide open. These checks take 3 minutes.
Your Supabase URL and anon key ship in the client bundle by design — that's normal. The thing actually protecting your data is Row Level Security (RLS): database-level policies that scope every query to the authenticated user. No RLS means anyone who knows your Supabase URL can query every row in every table.
If you're on Firebase, open the Firebase Console → Firestore → Rules. If you see `allow read, write: if true` — your database is public.
If your app has Stripe, verify that payment notifications are verified. Without this, anyone can fake a "payment successful" message.
Stripe sends your app a webhook when a payment completes. Your handler must verify the Stripe-Signature header before trusting the payload — otherwise anyone can POST a fake "payment successful" event and unlock paid features for free. Run the test below: if your server accepts an unsigned event, it's unprotected.
Security headers tell browsers how to protect your users. They're a 2-minute config change that prevents entire classes of attacks.
Check your current headers with the curl command below. If nothing comes back, add the four essential headers in your hosting config.
CORS decides which origins can call your API from the browser. AI defaults to the wildcard (`*`) because it makes local dev painless — and ships straight to production. For anything behind a login, restrict it to your own domain.
AI assistants put API keys where they don't belong. These checks find them before someone else does.
Open your deployed app, open your browser's developer tools (right-click anywhere → Inspect → Sources tab), and search all files for secret key patterns. Or paste the command below into your terminal.
AI assistants frequently place API keys directly in code that runs in the browser — visible to anyone. If someone finds your OpenAI key, they can spend your money. If they find your Stripe secret key, they can issue refunds.
# Paste this into your terminal — it checks your live site for leaked keys
curl -s https://yourapp.com | grep -oE \
'sk_live_[A-Za-z0-9]{20,}|sk-[A-Za-z0-9]{30,}|service_role|AKIA[A-Z0-9]{16}'
# Search your local code for secret key patterns
# (service_role = Supabase's all-access backend key, must never be public)
grep -rn 'sk_live_\|sk-\|service_role\|AKIA' --include='*.ts' --include='*.tsx' --include='*.js' src/Variables prefixed with NEXT_PUBLIC_ (Next.js), VITE_ (Vite), or EXPO_PUBLIC_ (Expo) are bundled into your client JavaScript — anyone viewing source can read them. That's fine for publishable keys. It's catastrophic for secret keys.
# List every public-prefixed env var — each one ships to the browser
grep -E '^(NEXT_PUBLIC_|VITE_|EXPO_PUBLIC_)' .env .env.local 2>/dev/null
# Safe to expose (scoped by design):
# NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
# NEVER expose (full backend access):
# DATABASE_URL, SUPABASE_SERVICE_ROLE_KEY, STRIPE_SECRET_KEY, OPENAI_API_KEY, webhook secretsA login page is not authentication. These checks verify your API actually rejects unauthorized requests.
Log in, open your browser's DevTools Network tab, find any API request. Right-click → Copy as cURL. Strip the Authorization header and re-run it. If you still get data back, your auth is cosmetic — the login page exists, but the API behind it has no real protection.
AI tools generate login pages that look secure, but the API endpoints behind them often have no real protection. An attacker doesn't use your login page — they call your API directly.
# Step 1: Find an API call in your Network tab
# Step 2: Copy as cURL
# Step 3: Remove the Authorization header and re-run:
curl https://yourapp.com/api/users
# If this returns user data → your API has no auth
curl https://yourapp.com/api/orders
# If this returns orders → anyone can read all ordersCreate two accounts. As User A, grab an ID from any API call (watch the Network tab). Log in as User B and request User A's resource by pasting that ID into the URL. If it returns data, you have an IDOR (Insecure Direct Object Reference) — the endpoint looks up records by ID without checking who owns them.
A checklist tells you what to do. A scan proves you did it. Paste your URL and verify everything in minutes.
IDOR is the #1 web vulnerability. AI generates endpoints that fetch by ID without checking who's asking. If User A's order is at /api/orders/123, User B just tries /api/orders/123 and gets it.
# As User B, try to fetch User A's data:
curl https://yourapp.com/api/orders/USER_A_ORDER_ID \
-H "Authorization: Bearer USER_B_TOKEN"
# If this returns User A's order → anyone can read anyone's data
# Fix: every database query must filter by the logged-in user's IDIf you're on Supabase or Firebase, your database might be wide open. These checks take 3 minutes.
Your Supabase URL and anon key ship in the client bundle by design — that's normal. The thing actually protecting your data is Row Level Security (RLS): database-level policies that scope every query to the authenticated user. No RLS means anyone who knows your Supabase URL can query every row in every table.
In our scan of 100 vibe-coded apps, 68% of Supabase apps had missing or broken RLS. Without it, anyone who knows your Supabase URL can query your database directly and read everything.
-- In your Supabase dashboard → SQL Editor → run:
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public';
-- Every row where rowsecurity = false is a wide-open table.
-- Fix: enable RLS and add a policy scoped to the authenticated user
ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users read own rows" ON your_table
FOR SELECT USING (auth.uid() = user_id);If you're on Firebase, open the Firebase Console → Firestore → Rules. If you see `allow read, write: if true` — your database is public.
// ❌ This means your database is wide open:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}
// ✅ Fix: require authentication
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth != null
&& request.auth.uid == userId;
}
}
}If your app has Stripe, verify that payment notifications are verified. Without this, anyone can fake a "payment successful" message.
Stripe sends your app a webhook when a payment completes. Your handler must verify the Stripe-Signature header before trusting the payload — otherwise anyone can POST a fake "payment successful" event and unlock paid features for free. Run the test below: if your server accepts an unsigned event, it's unprotected.
# Send a fake Stripe event — this SHOULD be rejected
curl -X POST https://yourapp.com/api/webhooks/stripe \
-H "Content-Type: application/json" \
-d '{"type":"checkout.session.completed","data":{"object":{"id":"cs_fake"}}}'
# If your server returns 200 → your webhook is unprotected
# Fix: verify the signature
import Stripe from 'stripe';
const event = stripe.webhooks.constructEvent(
body,
request.headers['stripe-signature'],
process.env.STRIPE_WEBHOOK_SECRET // server-side only
);Security headers tell browsers how to protect your users. They're a 2-minute config change that prevents entire classes of attacks.
Check your current headers with the curl command below. If nothing comes back, add the four essential headers in your hosting config.
# Check current headers
curl -sI https://yourapp.com | grep -iE \
'x-frame|content-type-options|strict-transport|content-security'
# Add these in vercel.json, _headers (Netlify), or next.config.js:
# X-Frame-Options: DENY → prevents clickjacking
# X-Content-Type-Options: nosniff → prevents MIME sniffing
# Strict-Transport-Security: max-age=31536000 → forces HTTPS
# Referrer-Policy: strict-origin-when-cross-origin → prevents URL leakageCORS decides which origins can call your API from the browser. AI defaults to the wildcard (`*`) because it makes local dev painless — and ships straight to production. For anything behind a login, restrict it to your own domain.
# Check your CORS headers:
curl -sI -H "Origin: https://evil.com" https://yourapp.com/api/users | grep -i access-control
# If you see Access-Control-Allow-Origin: * → fix it
# Only allow your real domains:
# Access-Control-Allow-Origin: https://yourapp.com