Anatomy of a Vibe-Coded App Breach: What Went Wrong and How to Prevent It
Setting the Scene
Imagine a SaaS app called "TaskFlow" — built in 3 days with Lovable and Cursor AI.
It's a project management tool with team workspaces, file uploads, and Stripe billing.
It launched last Monday. It was breached by Friday.
This is a synthetic walkthrough based on real vulnerability patterns we observe in vibe-coded apps.
No real company is named or harmed — this is for education.
Phase 1: Reconnaissance (Day 1, 10 minutes)
The attacker starts by running the app through automated scanners.
What they find:X-Powered-By: Next.js 15.1.0 header exposed (exact version fingerprinting)/.well-known/security.txt — doesn't exist (no responsible disclosure program)/_next/static/chunks/app-*.jscurl -s https://taskflow.app/_next/static/chunks/main-*.js | grep -Eo '[A-Za-z0-9_-]{20,}' | grep -v 'class\|function\|return'
Result: Nothing yet. But the bundle contains API endpoint paths.
Phase 2: IDOR Discovery (Day 2, 30 minutes)
The attacker creates a free account and browses the app normally.
They open the Network tab in Chrome DevTools.
Vulnerable API call observed:GET /api/tasks?projectId=proj_c4f8a2b1
They change projectId to values they don't own:
curl -H "Authorization: Bearer eyJ..." "https://taskflow.app/api/tasks?projectId=proj_000000001"
Result: 200 OK. Full task list for another user's project.
The backend code (generated by Cursor):
// ⚠️ No ownership check
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const projectId = searchParams.get('projectId');
const { data } = await supabase
.from('tasks')
.select('*')
.eq('project_id', projectId); // ← user controls projectId, no auth check
return Response.json(data);
}
Fix:
// ✅ Always check ownership
const session = await getServerSession();
const { data } = await supabase
.from('tasks')
.select('*')
.eq('project_id', projectId)
.eq('owner_id', session.user.id); // ← ownership enforced
Phase 3: Supabase RLS Bypass (Day 3)
The attacker inspects API responses and notices Supabase URLs in the response headers.
They find the anon key in the JavaScript bundle and connect directly to Supabase.
The projects table has RLS enabled — but via the service role key used in a leaky Edge Function, they bypass it:
Direct Supabase API call with anon key — normally blocked by RLS
curl "https://xxxxx.supabase.co/rest/v1/projects?select=*" -H "apikey: eyJanon..." -H "Authorization: Bearer eyJanon..."
Returns: {"code":"42501","message":"new row violates row-level security"}
Good — RLS is blocking direct access. But then they find the Edge Function:
curl "https://taskflow.app/api/export-project" -H "Authorization: Bearer eyJuser_token..." -d '{"projectId": "proj_000000001"}'
This Edge Function uses the service role key internally and doesn't check project ownership — full data dump.
Phase 4: File Access (Day 4)
The file upload endpoint stores files with predictable paths:
/uploads/{userId}/{filename}
The storage bucket is public. By guessing common filenames (avatar.png, export.csv, backup.zip),
the attacker downloads sensitive user files.
Phase 5: Escalation Attempt (Day 5)
In the admin dashboard, a route GET /api/admin/users is protected by:
if (user.role !== 'admin') return Response.json({ error: 'Forbidden' }, { status: 403 });
But the attacker found the role is stored in the JWT token — and Cursor generated a JWT secret
that was the word "secret". They forge a JWT with "role": "admin".
Prevention Summary
| Attack | Root Cause | Fix |
|--------|-----------|-----|
| IDOR | No ownership check in API | .eq('owner_id', session.user.id) |
| RLS Bypass | Service role key in Edge Functions | Use user JWT for all operations |
| File Access | Public storage bucket | Private buckets + signed URLs |
| JWT Forging | Weak JWT secret | Use 256-bit random secret |
Run a SecuriSky Scan
Our scanner automatically detects all 4 vulnerability classes from this walkthrough — IDOR patterns in API routes, service role key usage, public storage buckets, and weak JWT configuration.
Run a free scan → · See all 18 attack surfaces →