Case Studies

Anatomy of a Vibe-Coded App Breach: What Went Wrong and How to Prevent It

SecuriSky TeamApril 9, 202610 min read

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)
  • Public JavaScript bundled at /_next/static/chunks/app-*.js
  • Initial signal: The app is clearly AI-built. The attacker loads the JavaScript bundle:
    curl -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".

    Result: Full admin access.

    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 →
    How Vibe-Coded Apps Get Breached: A Complete Walkthrough (2025) — SecuriSky Blog