The DM that started this
It started on a Saturday night when a founder sent me a DM: "I just got on Product Hunt front page. 847 upvotes. Also, I think someone is reading my users' data."
She had built her app with Lovable. Three weeks of weekend sessions, zero security reviews. By the time she messaged me, someone had already discovered that /api/users/{id} required zero ownership check. They looped through IDs 1 to 10,000 and pulled first names, emails, and subscription tiers for every user on her platform.
She wasn't the exception. She was the pattern.
That conversation sent me down a rabbit hole. I reached out to builders in the Lovable community Discord, the Cursor builders channel, the Replit showcase thread. I asked founders if they'd let me run a scan against their MVPs. Not a sales pitch — I just wanted to understand the shape of the problem. One hundred said yes. I ran the same attack-surface playbook Sable runs on every paid scan against every one of them, with explicit written permission.
Here's what I found.
The numbers
73 out of 100 apps had at least one BOLA (Broken Object-Level Authorization). That's not "potential weakness" — that's a confirmed exploitable authorization flaw where one user can directly access or modify another user's data by changing an ID in the request.
41 had a missing or misconfigured Content Security Policy header. No CSP means a successful XSS anywhere on your site can exfiltrate session cookies or inject malicious scripts with no additional friction.
19 had a prompt injection chain in their AI features. Apps that feed user-supplied text to an LLM without sanitization are vulnerable to attacks where the model is instructed to leak system prompts, change its behavior, or exfiltrate data it was told to protect.
31 had CORS configured with a wildcard plus credentials. Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true is a contradiction — you're telling the browser "trust any site" while also "pass the user's cookies." Any malicious third-party site can make authenticated requests on behalf of your users.
22 had at least one admin endpoint reachable without elevated authorization. Routes like /api/admin/users, /api/admin/reports, or /dashboard/admin were accessible to any authenticated user, not just admins — because the auth check only verified "is logged in," not "is admin."
Why vibe coding has this problem
Let me be direct: the tools are not at fault. Lovable, Cursor, Replit, Bolt — they build real, functional apps faster than anything I've ever seen. The problem is structural.
When you tell Cursor "build me a SaaS for managing freelance invoices," the model does what you asked. It generates a working auth flow, a database schema, and API routes. It does not, by default, ask "should every user be able to read every other user's invoice?" The model optimizes for it works before it optimizes for it's safe.
Authorization bugs — especially BOLA — are invisible to happy-path tests. Your Playwright script logs in as user A and checks that user A can see their invoices. It passes. It never asks whether user A can also see user B's invoices by changing the ID in the URL. That test isn't in the happy path, so the model never writes it.
The result: you ship an app that works beautifully and leaks data quietly.
The three patterns I kept seeing across the 100 apps:
Pattern 1: The "just check if logged in" shortcut. The auth middleware confirms the token is valid. Then the route handler fetches the resource by ID from the database without checking who owns it. Every logged-in user can read every resource.
Pattern 2: The "frontend hides it so it's fine" assumption. Admin controls aren't shown in the UI to regular users. But the API route behind the admin button has no server-side role check. Hide the button, expose the endpoint.
Pattern 3: The "I got an A on SecurityHeaders.com" false confidence. SecurityHeaders.com grades your headers. It doesn't test your authorization logic. It doesn't chain findings. A+ on headers, BOLA in the API — this was the combo on 18 of the 73 apps with BOLA.
Three patches every vibe-coded app should ship before launch
Patch 1: Per-tenant scoping on every data-access route
The fix is one function call you add to every route that touches user-owned data.
Before (vulnerable):
// GET /api/invoices/:id
export async function GET(req: Request, { params }: { params: { id: string } }) {
const invoice = await db.invoice.findUnique({ where: { id: params.id } })
return Response.json(invoice)
}
After (fixed):
// GET /api/invoices/:id
export async function GET(req: Request, { params }: { params: { id: string } }) {
const session = await getServerSession(authOptions)
if (!session?.user?.id) return new Response('Unauthorized', { status: 401 })
const invoice = await db.invoice.findUnique({
where: { id: params.id, userId: session.user.id } // ownership check
})
if (!invoice) return new Response('Not found', { status: 404 })
return Response.json(invoice)
}
The key addition: userId: session.user.id in the query filter. If the invoice doesn't belong to the requesting user, the database returns null and you return 404. The attacker learns nothing about whether the resource exists.
Patch 2: Server-side role check on every admin route
Before (vulnerable):
// POST /api/admin/delete-user
export async function POST(req: Request) {
const session = await getServerSession(authOptions)
if (!session) return new Response('Unauthorized', { status: 401 })
// missing: role check
const { userId } = await req.json()
await db.user.delete({ where: { id: userId } })
return Response.json({ ok: true })
}
After (fixed):
// POST /api/admin/delete-user
export async function POST(req: Request) {
const session = await getServerSession(authOptions)
if (!session) return new Response('Unauthorized', { status: 401 })
if (session.user.role !== 'admin') return new Response('Forbidden', { status: 403 })
const { userId } = await req.json()
await db.user.delete({ where: { id: userId } })
return Response.json({ ok: true })
}
One line. session.user.role !== 'admin' returns 403 before any data operation runs. Do this on every route in your /api/admin/ directory, not just the ones you think are sensitive.
Patch 3: CORS lockdown
Before (vulnerable — Replit and Bolt scaffolds default to this):
// next.config.js or middleware.ts
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
}
After (fixed):
const ALLOWED_ORIGINS = [
'https://your-app.com',
'https://www.your-app.com',
process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : null,
].filter(Boolean)
export function middleware(req: NextRequest) {
const origin = req.headers.get('origin') ?? ''
const isAllowed = ALLOWED_ORIGINS.includes(origin)
const res = NextResponse.next()
if (isAllowed) {
res.headers.set('Access-Control-Allow-Origin', origin)
res.headers.set('Access-Control-Allow-Credentials', 'true')
}
// no wildcard + credentials combination ever
return res
}
Explicit allowlist. Only your own domains. Credentials only when the origin is explicitly trusted.
The tools that built your app can't fix this for you
I want to be careful not to dunk on Lovable, Cursor, or Replit. I use Cursor every day. The product is remarkable. The point isn't that AI-assisted dev tools are dangerous — it's that authorization logic is a domain where the defaults are wrong and no tool is going to save you if you don't explicitly ask it to.
When I reviewed the 100 apps, I noticed a secondary pattern: the ones with the worst authorization problems were often the ones with the cleanest code. Cursor-generated Next.js is genuinely beautiful. TypeScript strict mode, proper typing, Zod validation on inputs. Everything except the one line that scopes the database query to the current user's tenancy.
The problem is invisible at code-review time too. You look at the handler, see that it checks for a session, and move on. You're not a security engineer and you're not running tests designed to answer "can user A read user B's data" — so you don't catch it.
The 73 founders who had a BOLA weren't careless developers. Most of them were good developers. They just didn't have the frame of reference to ask the right question. That's the gap Sable fills.
What a Sable scan actually does
When you run npx @sable/agent scan, the agent crawls your app's endpoints, authenticates as two separate test users, and tries to access user A's resources as user B by manipulating IDs, slugs, and object references. It also checks CORS against all origins, tests admin routes for role bypass, scans for secrets in the client-side JavaScript bundle, and tests the full header stack.
Surface findings are free. The attack chains — the narratives that show the three-step path from unauthorized API call to full account takeover — those are in the $29 Pre-Launch Check. The patch diffs (paste-into-Cursor ready) are in there too.
Run it before you launch
The scan takes 10 minutes. It's free. No sales call. No signup required. In your terminal. Run it before you post on Product Hunt.
npx @sable/agent scan https://your-mvp.com
Made by a dev who got tired of watching founders find out about their BOLA from a stranger on the internet, not from a tool they ran before launch.
The 73 founders who had a BOLA would have paid $29 to know before they launched. Most of them would have paid a lot more than that after.