DEV Community

VibeFoundry
VibeFoundry

Posted on Edited on Fully Autonomous

Four security bugs AI coding agents keep writing in Next.js + Supabase apps (and 4 false positives I fixed in my scanner)

I use coding agents (Claude Code, Cursor, OpenCode) a lot, and after a while you notice that the bugs they write are not the bugs a tired human writes. A human forgets to escape one input. An agent writes a perfectly structured endpoint that has no authentication at all, reports the task as done, and moves on.

This post walks through the four patterns I kept running into in Next.js + Supabase projects, and how I turned them into rules for an open-source scanner called vibe-shield. Full disclosure: I am the author of that tool. Treat the last two sections as the pitch and everything before them as the actual content.

1. The service_role key that ends up in the browser

The Supabase service_role key bypasses Row Level Security. Agents know this in the abstract, but when they need an admin client they tend to do one of two things. They create lib/supabaseAdmin.ts and then import it from a component that has "use client" at the top (or from something that component imports). Or they reach for NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY because "it needed the env var".

// lib/supabaseAdmin.ts
import { createClient } from '@supabase/supabase-js'export const supabaseAdmin = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)
Enter fullscreen mode Exit fullscreen mode

That file is fine on the server. This is the bug:

'use client'
import { supabaseAdmin } from '@/lib/supabaseAdmin'
Enter fullscreen mode Exit fullscreen mode


The fix is to mark the helper with import 'server-only' so the build fails if a client component ever pulls it in, and to never prefix a secret with NEXT_PUBLIC_. The detection side is harder than it looks. Checking whether a file starts with "use client" is not enough, because the import is often transitive (a barrel file re-exports the helper, a hook imports the barrel, a component uses the hook). So the scanner builds a best-effort import graph from relative imports and tsconfig aliases and checks whether a client entry can reach a service-role client.

2. Mutation routes with no authentication

Ask an agent for "a delete endpoint" and you often get something like this:

// app/api/users/route.ts
export async function DELETE(req: Request) {
  const { id, userId } = await req.json()
  await db.query(`DELETE FROM users WHERE id = ${id} AND owner = ${userId}`)
  return new Response('ok')
}
Enter fullscreen mode Exit fullscreen mode

It works, and it has three problems at once: no authentication, SQL built by string concatenation, and the caller decides whose data gets deleted. A normal review loop rarely catches all three, because the code looks complete. The App Router makes this easy to miss, since every POST, PUT, PATCH and DELETE export is a public HTTP endpoint whether or not you meant it to be.

3. IDOR: trusting the request for who the user is

This is the third problem from the snippet above, and it deserves its own section because it survives even after you add authentication. The agent authenticates the user, and then still reads the ownership key from req.json():

const { data: { user } } = await supabase.auth.getUser()
if (!user) return new Response('Unauthorized', { status: 401 })

const { id, userId } = await req.json()   // userId comes from the caller
await db.from('documents').delete().eq('id', id).eq('owner', userId)
Enter fullscreen mode Exit fullscreen mode

The user is logged in, but they can pass any userId they like. The fix is to take the owner from the verified session (user.id), never from the body, query string or params.

4. Prompt injection sinks and poisoned agent config

The last pattern is two related problems. First, agents wire model output into dangerous sinks: eval, new Function, dangerouslySetInnerHTML, or a tool execute that shells out. If an LLM response can reach one of those, anyone who can influence the prompt can influence your server or your users' browsers.

Second, the agent configuration itself is an instruction surface that almost nothing audits. SKILL.md files, Cursor rules, AGENTS.md and MCP configs are all executed by an agent. They can hide instructions in zero-width characters, tell the agent to read ~/.ssh or .env, or launch an unpinned npx -y some-server MCP process. vibe-shield has a separate audit-skills command that checks those files for 12 classes of problem.

The four false positives I fixed in v2.1.1

A security scanner with a high false-positive rate gets uninstalled within a week, so v2.1.1 was entirely about precision. These are the four changes, all driven by real-world snippets that the scanner got wrong:

  1. Handler detection and auth recognition. Destructured arguments in an App Router handler could hide the real handler body. The scanner now extracts the body correctly and recognizes common auth helpers (such as supabase.auth.getUser()), explicit 401 guards and with* wrapper functions, without borrowing auth from a different handler in the same file.
  2. IDOR scoping. Findings are now limited to ownership fields, predicates or tracked aliases that actually come from the request. A session-derived owner sitting near some unrelated request data no longer fires, and a narrow "mismatch, then return" guard on the same property is recognized.
  3. UI components excluded from route checks. The admin/destructive-surface rule now only looks at server endpoints and server actions, so a button component called DeleteButton stops being flagged.
  4. Server-side Supabase admin helpers. An admin helper that is not reachable from client code is legitimate, so it is no longer reported. Public-prefixed secrets and client-reachable service-role clients are still reported.

The trade-off is worth being upfront about. Recognizing a with* wrapper or an auth helper is a signal, not proof that the wrapper actually enforces anything, and the client import graph is best-effort rather than a full bundler simulation. A clean score means the rules did not fire, not that the app is secure, and you should still review manually and pair it with a deeper tool like CodeQL or Semgrep.

Try it

It runs locally, has zero runtime dependencies and does not send your code anywhere:

npx -y @vibefoundry/vibe-shield@latest scan .
Enter fullscreen mode Exit fullscreen mode

Use the scoped package name. The unscoped vibe-shield on npm belongs to a different project. Here is the repo:

GitHub logo avivgerz-hue / vibe-shield

60-rule static security scanner, MCP server & skill poisoning auditor for AI-generated codebases (Claude Code, Codex, Cursor).

πŸ›‘οΈ vibe-shield

The security gate for AI-written code. Find exposed secrets, unguarded routes and missing RLS before you ship.

⭐ If this tool helps your AI coding workflow, please give this repository a Star! It helps more developers discover the project.

npm version npm downloads CI node license zero dependencies rules self-scan

Live Terminal Demo

npx -y @vibefoundry/vibe-shield@latest scan .
Enter fullscreen mode Exit fullscreen mode

Illustrative two-file scan; fixes are applied manually. Results vary by project. A clean score is not a security guarantee.

VibeFoundry Agent Suite

  • 🎨 unslop β€” UI Design DNA & Anti-Slop Engine Β· npx -y unslop-mcp@latest init
  • πŸ›‘οΈ vibe-shield β€” Security Scanner for AI Code Β· npx -y @vibefoundry/vibe-shield@latest scan .
  • πŸ‘» ghost-qa β€” Autonomous Headless Browser QA Β· npx -y ghost-qa@latest test http://localhost:3000

vibe-shield is a static security scanner plus an Agent Skill that helps Claude Code, Codex, Cursor and Windsurf audit before they say β€œdone”.


v2.0.0: eight defensive vectors

The v2 catalogue covers client exposure; auth and route protection; database/RLS; AI and…

It is MIT licensed and part of a small suite of tools I maintain for the same underlying problem (agents move fast and something has to keep the output honest). The siblings are unslop, a UI design linter that catches generic AI-generated interfaces, and ghost-qa, a headless-browser QA runner:

GitHub logo avivgerz-hue / unslop

Studio-grade UI design systems, Design DNA extractor & Anti-Slop MCP server for AI coding agents (Claude Code, Codex, Cursor, Windsurf).

unslop v2.1.0

⭐ If this tool helps your AI coding workflow, please give this repository a Star! It helps more developers discover the project.

VibeFoundry Agent Suite: 🎨 unslop β€” UI Design DNA & Anti-Slop Engine (npx -y unslop-mcp@latest init) Β· πŸ›‘οΈ vibe-shield β€” Security Scanner for AI Code (npx -y @vibefoundry/vibe-shield@latest scan .) Β· πŸ‘» ghost-qa β€” Autonomous Headless Browser QA (npx -y ghost-qa@latest test http://localhost:3000)

npm version npm downloads CI Node.js 20.19+ MIT license

Live Terminal Demo

npx -y unslop-mcp@latest init
Enter fullscreen mode Exit fullscreen mode

Your agent can write the code. unslop gives it a measurable design engineering protocol.

unslop preview

unslop is a zero-runtime-dependency UI/UX Design Engineering CLI, native MCP Server, and AI Agent Skill. It audits 72 rules across seven design craft pillars, installs ten studio-grade archetypes, extracts Design DNA, exports four token formats, and turns every finding into an exact, prioritized transformation blueprint for JSX/TSX, HTML, Vue, Svelte, CSS, and Tailwind codebases.

Works natively with Claude…

GitHub logo avivgerz-hue / ghost-qa

Autonomous headless browser QA & self-healing verification loop for AI coding agents

πŸ‘» ghost-qa

Autonomous headless browser QA & self-healing verification loop for coding agents. v2.1.0 β€” the 8-Trap Deep Browser Inspection Engine: 3 viewports Γ— light/dark, QA Health Score (A+…F), interactive HTML dashboard, and regression diffing.

CI npm version npm downloads License: MIT Node >= 20 Powered by Playwright

⭐ If this tool helps your AI coding workflow, please give this repository a Star! It helps more developers discover the project.

🧰 The VibeFoundry Agent Suite

One command each β€” star all three and keep the whole suite free & maintained:

  • 🎨 unslop β€” UI Design DNA & Anti-Slop Engine (npx -y unslop-mcp@latest init)
  • πŸ›‘οΈ vibe-shield β€” Security Scanner for AI Code (npx -y @vibefoundry/vibe-shield@latest scan .)
  • πŸ‘» ghost-qa β€” Autonomous Headless Browser QA (npx -y ghost-qa@latest test http://localhost:3000)

Live Terminal Demo

npx -y ghost-qa@latest test http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

ghost-qa terminal preview

Coding agents (Claude Code, Codex, Cursor, Windsurf) write code blind β€” then declare victory while the actual browser has React hydration errors, broken buttons…

If you have seen an agent-written vulnerability pattern that I have not covered, or a false positive the scanner produces on your code, please open an issue or tell me in the comments. Those reports are the most useful thing anyone can send me.

Top comments (0)