Research & Insights

Security

Three security failures to check before shipping an AI-built Next.js or Supabase app.

2026-07-27 · 8 min read
The vibe code teardown: why your app can pass the demo and still fail security checks

The vibe code teardown: why your app can pass the demo and still fail security checks

Your app works.

The login flow works. The dashboard loads. The database returns the right records. The landing page looks good enough to share.

Then someone opens the browser developer tools.

They find a credential in the JavaScript bundle. They change an object ID in an API request and see another user's data. They query a Supabase table that was never protected with a row-level security policy.

The app was not broken in the way most people expect. It was broken while everything still looked finished.

That is the security problem with vibe-coded applications: a successful demo tells you that the feature works. It does not tell you whether the trust boundaries are correct.

The 45% number, stated accurately

Veracode's 2025 GenAI Code Security Report tested more than 100 large language models across Java, JavaScript, Python, and C#.

In its test set, 45% of generated code samples failed security tests and introduced an OWASP Top 10 vulnerability.

The Spring 2026 update used a continued testing framework and found that only about 55% of generation tasks produced secure code when the models received no security-specific guidance. In the other 45% of cases, the generated code introduced a known security flaw in the test task.

That does not mean 45% of all AI-built applications are vulnerable. The tests measured generated code in controlled tasks, not every application produced by every developer.

The result is still worth paying attention to:

AI coding tools have become much better at producing code that works. That improvement has not automatically made the code safe.

The gap matters because the most dangerous problems in a small web application are often not exotic. They are ordinary mistakes hidden behind a polished interface.

The demo is not the security boundary

AI coding tools are optimized to satisfy the request in front of them:

  • add authentication
  • connect the database
  • integrate payments
  • expose an API
  • create an admin dashboard
  • make the page work

Security depends on questions that may never appear in the original prompt:

  • Which user is allowed to read this record?
  • What happens if they change the ID in the request?
  • Which values can safely reach the browser?
  • Which database operations should the public client role perform?
  • Is this admin route protected outside the normal UI?
  • Does an error response reveal credentials or internal details?
  • Can a user perform an expensive action repeatedly?

A generated application can satisfy the visible feature request while leaving those questions unanswered.

That is how a working demo becomes a risky production system.

Here are three places I would check first.

1. The credential that ships to every browser

In a Next.js application, any environment variable prefixed with NEXT_PUBLIC_ is intended to be available to browser-side code.

That makes the prefix useful for public configuration. It also makes it dangerous when someone uses it for a secret.

This is a bad idea:

NEXT_PUBLIC_OPENAI_API_KEY=sk-live-example
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_example
NEXT_PUBLIC_DATABASE_PASSWORD=example

Those values can end up in the client bundle. Anyone who can load the application may be able to retrieve them.

The safe pattern is to keep secret credentials on the server:

OPENAI_API_KEY=sk-live-example
STRIPE_SECRET_KEY=sk_live_example

Then call the provider from a server-side route or server action.

There is an important distinction here: not every visible key is a secret.

For example:

  • Stripe publishable keys are designed to be public.
  • Supabase anon keys are designed to be public.
  • Stripe secret keys are not public.
  • Supabase service_role keys are not public.
  • Database passwords and private API tokens are not public.

The problem is not "a key appears in the frontend." The problem is that a credential with more authority than the browser should have crosses the client/server boundary.

What to check

Search the repository and the deployed JavaScript bundle for:

  • OpenAI or Anthropic API keys
  • Stripe secret keys
  • Supabase service-role keys
  • database credentials
  • private signing keys
  • unrestricted third-party API tokens
  • suspicious NEXT_PUBLIC_ variables

A source-only scan is not always enough. A value may be injected during the build and appear only in the generated client assets.

2. The database with no access policy

Supabase makes it fast to create tables, authentication, and a working frontend.

That speed creates a predictable failure mode: the application is wired together before the database access model is finished.

Row Level Security, or RLS, is the layer that controls which rows a database role can access. A table can exist, the frontend can query it, and the feature can appear complete while the table still lacks the policies that separate one user's data from another's.

The public Supabase anon key is not automatically a master key. It is meant to be public. The risk appears when that public client role has access to data that should have been protected.

A basic policy might look like this:

alter table public.notes enable row level security;

create policy "users can read their own notes"
on public.notes
for select
to authenticated
using (auth.uid() = user_id);

The exact policy depends on the application. The important point is that authentication alone does not prove authorization. A user can be logged in and still be unauthorized to read another user's record.

A real example

CVE-2025-48757 describes insufficient database Row Level Security policies in Lovable-generated sites through April 15, 2025. The NVD record says the condition could allow unauthenticated attackers to read or write arbitrary database tables.

The same record also notes that Lovable disputed the responsibility assigned to the platform and argued that individual customers were responsible for protecting their application data.

That disagreement is worth preserving because it points to the real lesson:

A generated app can look complete while its access-control model is still unfinished.

What to check

For every table:

  1. Is RLS enabled?
  2. Which roles can access it?
  3. Are there explicit policies for select, insert, update, and delete?
  4. Does every policy restrict access to the correct user, team, or organization?
  5. What happens if an authenticated user changes an object ID?
  6. Do tests run as a normal user, or only as an admin?

"RLS enabled" is not the same as "authorization is correct." The policies themselves need review.

3. The admin route nobody meant to publish

During development, an AI assistant may create routes such as:

/admin
/debug
/health/full
/swagger
/api/test

These routes can be useful while building. They become a problem when they reach production without an authentication and authorization check.

The same issue can appear in API handlers. The frontend may hide a button from ordinary users, but hiding a button is not authorization. A user can still call the endpoint directly.

A route should enforce access on the server, not depend on the interface behaving honestly.

What to check

Open the deployed application in a private browser window while logged out.

Test:

  • admin and dashboard routes
  • health and debug endpoints
  • API routes
  • file or export endpoints
  • password-reset and invitation flows
  • routes that accept a user ID, organization ID, or document ID

Then test horizontally:

  1. Create two normal users.
  2. Create a record for User A.
  3. Send the request as User B.
  4. Change the record ID.
  5. Confirm that User B receives a denial rather than User A's data.

This class of bug is often called insecure direct object reference, or IDOR. It does not require a sophisticated exploit. It requires the application to trust an identifier supplied by the client.

A practical pre-launch security check

You do not need to begin with a full penetration test to catch the most obvious problems in a small Next.js or Supabase application.

Start with these checks.

1. Sweep for credentials

Scan:

  • the repository
  • .env files
  • Git history
  • build output
  • deployed client assets
  • CI logs
  • error-monitoring payloads

Remove exposed secrets and rotate them. Deleting a key from the latest commit does not invalidate a key that already exists in Git history or a deployed bundle.

2. Review database access

For each table, verify:

  • RLS status
  • policies for every operation
  • user and organization boundaries
  • behavior for unauthenticated requests
  • behavior for a different authenticated user
  • behavior when IDs are modified

Test the database with the same roles that real users will have. An admin dashboard can make a broken policy look correct because the admin has more access than ordinary users.

3. Test routes outside the interface

Use an incognito window and direct HTTP requests.

Do not rely on:

  • hidden buttons
  • client-side redirects
  • disabled form fields
  • checks that run only in React
  • route names that "look private"

The server must make the final authorization decision.

4. Check browser security configuration

Review:

  • HTTPS
  • cookie flags
  • HttpOnly
  • Secure
  • SameSite
  • Content Security Policy
  • frame protection
  • MIME-sniffing protection
  • permissive CORS
  • unnecessary X-Powered-By disclosure

These settings will not fix a broken authorization model, but weak deployment defaults can make other failures easier to exploit.

Try Vibe Scanner

I built Vibe Scanner as a read-only scanner for Next.js and Supabase repositories and deployed websites.

It is designed to catch common problems before they become someone else's discovery.

The scanner currently checks repository targets for issues such as:

  • hardcoded Stripe, OpenAI, GitHub, or AWS credentials
  • sensitive environment files
  • Supabase tables without matching RLS enablement
  • admin or dashboard pages without a recognized authentication check
  • Next.js API handlers without a recognized authentication check
  • wildcard CORS
  • missing baseline browser security headers
  • suspicious NEXT_PUBLIC_* variables

It can also scan a public URL for:

  • reachability
  • HTTPS transport
  • deployed security headers
  • wildcard CORS
  • cookie flags
  • X-Powered-By disclosure

The scanner is intentionally read-only. URL scans make a public GET request chain, follow redirects, and do not crawl the application, submit forms, or attempt exploitation.

Install and run it locally

git clone https://github.com/CyprianTinasheAarons/vibe-scanner.git
cd vibe-scanner

python3 -m venv .venv
source .venv/bin/activate

python -m pip install -e .
vibe-scanner /absolute/path/to/your/project

To scan a deployed URL:

vibe-scanner https://your-domain.com

To produce a machine-readable report:

vibe-scanner --json /absolute/path/to/your/project > report.json

The JSON output includes:

  • scanner version
  • target type
  • normalized target
  • severity counts
  • complete findings
  • remediation context

Secret values are not included in the findings.

The scanner is a first pass, not a security certificate. Authentication, RLS, CORS, and header checks use static heuristics. A warning needs to be confirmed in the context of the application. A clean scan does not prove that the application is secure.

It means the scanner did not find the specific patterns it checks for.

What the scanner cannot replace

Vibe Scanner does not replace:

  • a full penetration test
  • threat modeling
  • manual authorization testing
  • business-logic review
  • dependency risk analysis
  • compliance certification
  • an experienced security engineer reviewing the system

That distinction matters. Security tools become dangerous when their output is treated as certainty.

A scanner should reduce the chance of missing obvious problems. It should not create false confidence.

The rule I would use before shipping

If you built an application quickly with Cursor, Bolt, Lovable, v0, Claude Code, or another AI tool, ask one question before you send more traffic to it:

Which parts of this application are trusted because the server verified them, and which parts are trusted because the browser said they were true?

That question leads you to the places that deserve attention:

  • secrets
  • database policies
  • API authorization
  • object ownership
  • admin routes
  • deployment configuration

A working demo is a good start. It is not evidence that the application is ready for strangers, payments, or private data.

Need a human review?

Topiax is building tools and services for teams that want to move quickly without treating security as an afterthought.

For small Next.js and Supabase applications, Topiax offers a Ship Confidence Review:

  • 48-hour turnaround
  • $750–$1,500 flat fee
  • focused review of secrets, authentication, authorization, RLS, API routes, and common deployment gaps
  • plain-English report with prioritized fixes
  • bounded review, not a penetration test or compliance certification

If you want a human to review the application, DM SHIP.

Sources