Vibe Coding Security Checklist: 12 Essential Checks

12 security checks for Vibe-Coded apps — 57% of testable databases are open to the public by default.
A scan of ~31,000 AI-built apps found 57% of testable Supabase databases readable without login. Using real incidents like Moltbook as examples, this article argues that Vibe-Coded app vulnerabilities stem from three default configuration errors — open databases, secrets bundled into the browser, and routes that never verify record ownership — not from AI code quality. It provides a 12-check checklist covering git history scanning, frontend bundle secrets, RLS verification, privilege escalation tests, dependency vulnerabilities, fake package detection, rate limiting, and response headers, with six checks making live requests against build output or URLs. It also covers integrating key checks into CI and using AGENTS.md to give coding agents persistent security rules.
When AI Writes Your Code, Who Keeps It Secure?
An automated scan of roughly 31,000 live apps built with Lovable, Bolt, v0, and Replit returned a startling result: 57% of the Supabase databases that could be tested were readable by anyone without logging in (Source: Reeve, August 2026). These databases were open to the entire internet by default, and no one had flipped the switch to close them.
The core problem isn't that AI-written code is particularly bad. A March 2026 study comparing tens of thousands of AI-generated and human-written code units from real repositories found that AI code actually triggered slightly fewer scan alerts and contained fewer hardcoded secrets (Mao et al., March 2026). The real vulnerabilities come from the configuration an app ships with — an open database, a secret bundled into the browser, a route that never checks who's calling. All three are configuration errors that can be detected with a command.

Why Vibe-Coded Apps Leak Data
The clearest cautionary tale this year is Moltbook. It was an AI social network that launched in late January, with a founder who claimed to have "not written a single line of code." Within four days of launch, Wiz researchers found database credentials sitting in the site's JavaScript, with row-level security disabled — meaning anyone could read and write the entire database. The result: roughly 1.5 million API tokens and tens of thousands of email addresses were exposed (Wiz, February 2026).
That same month, an exam platform featured on Lovable's own Discover page leaked 18,697 user records. Its login validation logic was inverted, the database had no row-level security, and user roles were never checked anywhere. The researcher who discovered the vulnerability called the main flaw "a classic logic inversion that any human security reviewer would catch in seconds" (The Register, February 2026).
Both incidents point to the same root cause: in the past, getting an app in front of users required you to configure authentication yourself, because nothing would work otherwise. AI tools skip that step, so apps run on default settings — and the default for a new database is "open."
How to Use This Checklist
Run all 12 checks before your first real user signs up, again before accepting your first payment, and monthly after launch. The commands assume a Next.js App Router project, npm, and a Unix shell (Windows users should use Git Bash or WSL). Checks 3 through 5 are Supabase-specific, since Supabase is the default backend most Vibe-Coding tools hand you.
One key insight runs throughout: most of the damage documented in this article happened outside the source code — in database settings, secrets that ended up in deployment files, and routes that responded to the wrong users. A scanner that only reads your repository won't see any of this. That's why 6 of the 12 checks make direct requests against your build output or live URL, and code scanning comes last. First, install four scanners:
brew install gitleaks trufflehog osv-scanner semgrep
Secrets: The Three Most Common Leak Points (Checks 1–3)
Secrets typically leak from three places: git history, JavaScript sent to the browser, and the wrong type of database credential. GitGuardian's 2026 report found that commits written with Claude Code were roughly twice as likely to contain leaked secrets as average public commits (GitGuardian, March 2026), and most secrets leaked years ago are still valid today because no one rotated them. So for every check in this section, the first step of the fix is the same: rotate the secret first, then clean up.
Check 1: Are There Secrets in Your Git History?
gitleaks git . --redact -v
trufflehog git file://. --results=verified --fail
Fail signal: gitleaks prints findings and exits with code 1. The trufflehog line narrows results to secrets the vendor has confirmed are still active. Rotating a secret takes minutes; rewriting history to remove it takes about an hour — rotation matters more, because secrets in public commits have already been copied.
Check 2: Are Any Secrets Bundled Into the Browser?
npm run build && grep -rIoE \
-e 'sb_secret_[A-Za-z0-9_-]+' -e 'sk_(live|test)_[A-Za-z0-9]+' \
-e 'sk-[A-Za-z0-9_-]{20,}' -e 'postgres(ql)?://[^:/]+:[^@]+@' \
.next/static | sort -u
Fail signal: anything is printed. Next.js copies every variable prefixed with NEXT_PUBLIC_ into the browser bundle in plaintext at build time. So when someone adds that prefix to STRIPE_SECRET_KEY to make an "undefined" error go away, the secret becomes public. This author once left a Stripe secret sitting in .next/static in plaintext for nine days, exactly this way.
Check 3: Which Type of Supabase Key Are You Sending Out?
Supabase has two types of keys. The publishable key (formerly anon) is designed for the browser and is constrained by row-level security. The secret key (formerly service_role) bypasses row-level security entirely — if it ends up in your bundle, you've handed over the entire database. Scan the bundle with grep, and decode the middle segment of any JWT token you find to check whether it contains "role":"service_role".
JWT (JSON Web Token) is a token made up of three Base64-encoded segments in the format
header.payload.signature. The middle segment (payload) contains plaintext claims and can be decoded in a browser console by runningatob('middle-segment-string')— no key required. Both Supabase key types are JWTs: the publishable key's payload contains"role":"anon", while the service_role key contains"role":"service_role". Internally, Supabase treats the latter as a superuser path that bypasses all RLS policies — this is by design, so that server-side admin scripts can operate without restriction. It should therefore only appear in server-side environment variables and never in any file sent to the browser.
Database: Is It Actually Locked Down? (Checks 4–5)
In a Supabase app, the browser talks directly to the database, so the database itself must decide who can read which rows. That job belongs to row-level security (RLS), which is off by default on every table you create with SQL. That opening 57% figure measures exactly this: open tables.
Check 4: Which Tables Have RLS Disabled?
select tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by rowsecurity, tablename;
Fail signal: any row shows rowsecurity = f. Then open Supabase's Security Advisor dashboard and prioritize 0013_rls_disabled_in_public, 0002_auth_users_exposed, and 0024_permissive_rls_policy (policies with USING (true)).
Check 5: Prove It From the Outside
curl "https://abcdefghijkl.supabase.co/rest/v1/orders?select=*&limit=5" \
-H "apikey: sb_publishable_AbC123..."
Fail signal: real data rows are printed. An empty array or an error object with code 42501 confirms RLS is working. This is the same request used to discover the Moltbook database and the open databases in Reeve's scan.
Row-Level Security (RLS) is a feature introduced in PostgreSQL 9.5 that lets the database automatically attach a filter to every read and write operation. In Supabase's case, when a browser queries the database directly through the PostgREST API, there is no intermediate server layer to intercept requests and verify ownership — RLS is the only line of defense. Enabling RLS puts a table into a "deny all" state by default; you must explicitly create policies to allow access. A typical policy that lets users only read their own data looks like:
CREATE POLICY "users_own_rows" ON orders USING (auth.uid() = user_id);.USING (true)means all rows pass unconditionally, which is equivalent to having RLS disabled — and that's exactly what the0024_permissive_rls_policyalert in Check 4 catches.
Authorization: Whose Record Is This? (Checks 6–7)
Broken access control tops the OWASP Top 10:2025. In plain terms, the app verifies "you're logged in" and then hands over a record without checking who it belongs to. This was the shared flaw in both Lovable incidents this year.
Check 6: How Many Route Handlers Never Check Authorization?
find app src/app -name 'route.*' 2>/dev/null | wc -l
grep -rlE 'auth\(\)|protect\(|getSession\(|getClaims\(|getUser\(' \
--include='route.*' app src/app 2>/dev/null | wc -l
Fail signal: the second number is much smaller than the first. Note that Clerk's clerkMiddleware() does not protect anything on its own — the official docs explicitly recommend placing access checks "as close to the resource as possible."
Check 7: Can User A Read User B's Records?
The no-terminal version is simple: log in as User A, open an orders or settings page that belongs to you, then replace the id in the address bar with a record id belonging to User B. If the page loads normally, you've failed. The terminal version is to request User B's record using User A's access token with curl:
curl -si 'https://app.example.com/api/orders/9f2c' \
-H 'cookie: __session=eyJhbGciOi...' | head -1
Fail signal: the request returns 200 for User B's id. You want to see an empty array, a 403, or a 404. The fix is to add an ownership condition to every query, such as where: { id, organizationId }. This is the fix AI tools most often skip, because nothing appears broken on the surface when it's missing.
Dependencies: Known Vulnerabilities and Fake Packages (Checks 8–9)
Your package.json may be hiding two types of problems: packages with known vulnerabilities, and packages that never actually existed.
Check 8: Do Your Published Assets Contain Known Vulnerabilities?
npm audit --audit-level=high --omit=dev
osv-scanner scan source -r .
--omit=dev (the replacement for the old --production flag) removes build tool noise from the report. osv-scanner catches things npm audit misses — packages from other ecosystems and advisories npm hasn't yet mirrored.
Check 9: Is Every Dependency a Real Package?
AI models hallucinate package names, and they hallucinate the same wrong names repeatedly. A USENIX Security 2025 study found that commercial models invent a package name roughly once every 20 suggestions, and open-source models roughly once every 5 (Spracklen et al., 2025). Attackers register these fictional names and wait — a technique called slopsquatting. In February 2026, npm placed unused-imports (a name models generate when they mean eslint-plugin-unused-imports) under a security hold. Adding npm ci --ignore-scripts to CI prevents the next fake package from running code during installation.
A supply chain attack targets an application not by attacking it directly, but by poisoning an upstream component it depends on. Slopsquatting is a newer variant: attackers monitor the misspelled or invented package names that AI models repeatedly generate, register those names on npm and similar registries, and embed malicious code (such as environment variable exfiltration or a reverse shell). Because users of AI tools often run
npm installwithout verifying package origins, the malicious package installs silently.npm ci --ignore-scriptsprevents packages from executingpreinstall/postinstallhooks during installation — the vast majority of installation-time malicious code runs through these hooks, and legitimate packages rarely need install scripts.
Rate Limiting, Response Headers, and Code Scanning (Checks 10–12)
Check 10: Is Anything Rate-Limited?
for i in $(seq 1 40); do
curl -s -o /dev/null -w "%{http_code} " \
'https://app.example.com/api/generate' \
-H 'content-type: application/json' -d '{"prompt":"hi"}'
done; echo
Fail signal: all 40 responses come back without a single 429. Make sure to run this against a preview deployment, not production.
Check 11: What Else Is Your Host Serving?
Probe whether dotfiles like /.env or /.git/config return 200, check whether security response headers like HSTS, CSP, and X-Frame-Options are present, and check whether .map source map files are exposed in .next/static. Source maps hand over your unminified code in full — roughly one in eight Vibe-Coded apps in Reeve's scan was serving source maps.
Check 12: Run a Code Scanner
semgrep scan --config p/owasp-top-ten --config p/secrets --error .
Code scanning comes last precisely because it cannot see the configuration issues — the ones that caused every real incident in this article — that the previous 11 checks cover.
Let Your Coding Agent Run the Checks
10 of the 12 checks are shell commands, so a coding agent with terminal access (Claude Code, Cursor, Codex) can run them and produce a report for you. Two rules keep this safe: give the agent a preview URL, not production, because Check 7 reads real data and Check 10 sends 40 requests; and keep the agent read-only, because agents with edit access sometimes "fix" findings by weakening the checks themselves.
AGENTS.md (sometimes written as CLAUDE.md or .cursor/rules) is a file placed in the project root that sets persistent instructions for coding agents. Agents read it at the start of every new session, effectively injecting permanent project rules into the model. Writing security rules here rather than in chat messages prevents them from being "forgotten" during long conversations — LLM attention weights for early content decrease over very long contexts. Typical security rules include:
NEVER disable RLS on any table,NEVER prefix secret keys with NEXT_PUBLIC_, andALWAYS add ownership filter (user_id = auth.uid()) to queries. These rules form a prevention layer that reduces the creation of new vulnerabilities, while the check commands in CI form a detection layer that surfaces existing ones. The two complement each other rather than one replacing the other.
Putting the Checks into CI
Running the checklist once only catches today's problems. Putting it in CI means the checks also run on every line of code the AI commits next month. Three checks should go into CI right now: run gitleaks on every push, run npm audit and Semgrep on every PR, and use npm ci --ignore-scripts for the install step. The four live checks (5, 7, 10, 11) can be run manually for about twenty minutes each month, since they catch configuration changes that never go through a commit.
The "never" rules in AGENTS.md — never disable RLS, never hardcode secrets, never skip ownership checks — reduce the number of findings. Wrapping the checks in guardrails outside the model ensures the gates run regardless of whether the model remembers the rules. Vibe-Coded apps at any scale need both.
Related articles

Felix Rieseberg Personal Homepage
Felix Rieseberg's personal homepage sparks attention on Hacker News. This article covers the Slack engineer's technical background, the rise and fall of nativefier, and the value of a developer homepage as a tech calling card.

@ai-sdk/vue 3.0.306 Patch Release Syncs with [email protected] Update
@ai-sdk/vue 3.0.306 released, synced with [email protected]. A routine patch release for the Vercel AI SDK ecosystem with no public API changes; the Vue adapter layer stays version-aligned with the core package.

Gemini 4 Argon API Launches: The Era of Million-Token Output Begins
Google launches Gemini 4 Argon with sparse MoE architecture, up to 1M output tokens, and a 68.90% Vals Index score, available across API, AI Studio, and the Gemini app.