BackShipLabs
Strategic Briefs
Application Security
October 23, 20268 min read

Five Security Holes Common in Vibe-Coded Apps (and How to Check for Each)

Focus Area
Production Readiness
System Architecture
Next.js · Supabase · Stripe
Publisher
ShipLabs Engineering

Tools like Cursor, Lovable, Bolt, v0, and Replit have changed who gets to ship software. A founder with a clear idea can have a working product in front of customers in days. That's a real achievement, and nothing in this post argues against it.

The catch is that these tools optimize for the thing you can see: the page renders, the button works, the data shows up. Security is mostly about what you can't see. It's about what happens when someone uses your app in a way you didn't intend, such as changing an ID in a URL, calling your API directly, or sending a thousand requests a minute. An AI assistant won't test for that unless you ask, and most people don't know to ask.

The five problems below are the ones that come up most often in apps built this way. None of them need a security background to check. Each section explains the problem, how to find out whether you have it, and what the fix looks like.

1. Your database is open to anyone with the public key

Most vibe-coded apps use Supabase or Firebase, and both let the browser talk to the database directly. That's convenient, but it means the key in your frontend code is public by design. Anyone can open developer tools and copy it. The only thing between that key and your entire database is the access rules you've written: row-level security (RLS) policies in Supabase, or security rules in Firebase.

If those rules are missing, or set to allow everything, any visitor can read every row in every table. This isn't hypothetical. In 2025 a researcher scanned 1,645 apps built with Lovable and found about 170 whose Supabase tables could be read or written using only the public key. The issue was assigned CVE-2025-48757. Lovable disputes it, arguing that each customer is responsible for their own data, and in a sense that's the point: the platform won't necessarily protect you.

How to check: in Supabase, open the Security Advisor in your project dashboard, which flags tables in the public schema with RLS disabled. In Firebase, open your Firestore and Storage rules and look for anything like allow read, write: if true, or test-mode rules with an expiry date.

2. Secret keys are shipped to the browser

Frameworks decide which environment variables reach the browser by their name. In Next.js, anything starting with NEXT_PUBLIC_ is bundled into client code. In Vite, it's VITE_. When an AI assistant hits an error like "key is undefined" in a component, the quickest fix is to add that prefix. The error goes away, and your Stripe secret key, OpenAI key, or Supabase service role key is now readable by anyone who visits the site.

How to check: look for secret-sounding variables with a public prefix, then search the production build for known key formats.

Find secrets that are exposed to the clientbash
# 1. Public-prefixed variables that look like secrets
grep -rEn "^(NEXT_PUBLIC|VITE)_[A-Z_]*(SECRET|SERVICE_ROLE|PRIVATE|STRIPE_SK)" .env*

# 2. Known secret key formats in the built client bundle
npm run build
grep -rEo "sk_live_[A-Za-z0-9]{10,}|sk-proj-[A-Za-z0-9_-]{10,}|sb_secret_[A-Za-z0-9_-]{10,}|AKIA[0-9A-Z]{16}" \
  .next/static dist 2>/dev/null | sort -u

The fix: move the call that needs the secret into a server route or server action, and remove the prefix. Then rotate the key in the provider's dashboard. Rotating matters. If a key was ever in a public bundle or a public Git repository, assume someone has a copy.

3. Authorization only happens in the interface

This is the most common serious bug in web apps generally, not just AI-built ones, and the one automated scanners are worst at finding. The app hides the admin button from regular users, or only shows each user their own invoices, but the API behind it never checks. Anyone who calls the endpoint directly, or changes /api/invoices/1042 to /api/invoices/1043, gets data they shouldn't.

How to check: log in as one test user, copy a request from your browser's network tab, and replay it while logged in as a different test user, or with the ID changed. If you get data back, the check is missing.

Every lookup by ID checks ownership on the serverts
// app/api/invoices/[id]/route.ts
export async function GET(
  req: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  const user = await getCurrentUser(req); // your auth helper
  if (!user) return new Response("Unauthorized", { status: 401 });

  const invoice = await db.invoice.findUnique({ where: { id } });

  // Same response for "missing" and "not yours", so IDs can't be probed
  if (!invoice || invoice.ownerId !== user.id) {
    return new Response("Not found", { status: 404 });
  }

  return Response.json(invoice);
}

The rule of thumb: the browser decides what to show, the server decides what's allowed. Every API route and server action should check who the user is and whether they're allowed to touch this specific record.

4. Nothing limits how often expensive endpoints can be called

Sign-up, password reset, one-time codes, and especially AI features all cost money each time they run. Without rate limits, a single script can send thousands of sign-up emails, run up your SMS bill, or burn through a month of model credits overnight. This is one of the most common ways a newly launched AI app gets hurt, and it often looks like a billing problem rather than a security one.

How to check: pick your most expensive endpoint and ask what would stop someone calling it 10,000 times in an hour. If the answer is nothing, add a limit.

Per-user rate limit on an AI endpointts
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, "1 m"), // 10 requests per minute
});

export async function POST(req: Request) {
  const user = await getCurrentUser(req);
  if (!user) return new Response("Unauthorized", { status: 401 });

  const { success } = await ratelimit.limit(user.id);
  if (!success) return new Response("Too many requests", { status: 429 });

  // ...call the model
}

Also set a hard monthly spending limit in your AI provider's dashboard. A rate limit controls how fast money goes out; a spending cap controls how much.

5. The app trusts the browser about money

Two patterns show up repeatedly. First, the checkout sends the price from the browser, so a user can edit the request and pay one cent. Second, the payment webhook doesn't verify that the request actually came from Stripe, so anyone who finds the URL can mark an order as paid.

How to check: search your checkout code for a price or amount that comes from the request body. Then open your webhook handler and look for a signature check.

Verify the Stripe signature before trusting a webhookts
// app/api/webhooks/stripe/route.ts
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(req: Request) {
  const body = await req.text(); // raw body, not req.json()
  const signature = req.headers.get("stripe-signature");
  if (!signature) return new Response("Missing signature", { status: 400 });

  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch {
    return new Response("Invalid signature", { status: 400 });
  }

  // Safe to act on event.type now
}

For checkout, send only a product or price ID from the browser and look up the amount on the server.

Making the fixes stick

Fixing these once is the easy part. The harder part is that the next AI-generated change can quietly undo a fix, because the assistant doesn't remember why the code was written that way. A few habits help:

  • Write a test for each fix, such as "user B gets a 404 for user A's invoice", so a regression fails the build.
  • Run secret scanning and dependency checks in CI, not just on your laptop.
  • Keep your database rules in migration files under version control, not only in the dashboard.
  • Tell your AI assistant the rules explicitly, in a project instructions file that every session reads.

Where we stand: ShipLabs runs security assessments and production hardening for apps built with AI tools. Details of CVE-2025-48757 come from the public CVE record and published research. Only run these checks against applications you own or have written permission to test.

Checks Covered

"Database access rules on every table · No secret keys in the client bundle · Authorization checked on the server · Rate limits on expensive endpoints · Verified payment webhooks"

Related Engineering Service

Security Assessments & Production Hardening

We assess apps built with AI tools the way an attacker would, then fix authorization, database policies, secrets, tests, and monitoring without a rewrite.

Explore Technical Architecture
Related Strategic Briefs
Application Security
Supabase Row-Level Security: The Mistakes That Expose Every User's Data

In Supabase, the key in your frontend is public by design. Row-level security is the only thing standing between it and your users' data. Here are the five mistakes that break that protection, how to find them in your own project, and the SQL to fix each one.

Read Brief
AI Architecture
Decision Models for the Enterprise: Jev, Clef, and OpenAI's Decisions API

Jev, Clef, and OpenAI's Decisions API shipped within three weeks of each other, and none of them write text. Here's what the requests actually look like, how the three differ, and the adapter and cascade we'd put around them in production.

Read Brief
Finance Operations
Invoice Reconciliation & Payment Controls at Scale

Enterprise finance teams spend thousands of manual hours reconciling supplier invoices against purchase orders, goods receipts, and payment ledgers. Here is how we automated it.

Read Brief
Commerce Operations
Multilingual Voice Orders & Demand Forecasting

High-volume restaurants and service networks lose order potential during peak traffic because their teams cannot answer every call. We built low-latency multilingual voice systems linked directly to demand and procurement forecasts.

Read Brief
Risk & Governance
Regulatory Change Management with Deterministic RAG

Regulatory requirements can change overnight. We built deterministic Graph RAG architectures to monitor authoritative updates and automatically suggest redlines to existing policies and agreements.

Read Brief
Direct Consult

Have similar structural bottlenecks?

We deploy highly deterministic agent systems directly into enterprise database and pipeline topologies. Drop us your specifications or book a technical strategy sync.

Technical Strategy Sync

Book a direct 30-minute sync to dissect your pipeline schemas:

Book 30-Min Call
Drop a Brief

Send details of your architecture directly to our developer team:

hello@shiplabs.app