Browse the blog
Sentrint Blog
v0 · Checklist

A v0 security checklist for the Next.js app you just exported

Nine checks for a v0 project, ordered by what an attacker reaches first. Pin a patched Next.js version, move authorisation out of middleware and into the handler, check every server action for a caller check, and confirm no secret crossed into a client component. Then test cross-account reads with two real accounts.

v0 writes clean, idiomatic Next.js with the App Router and shadcn components, and that is genuinely most of the value. It also means the security failures you inherit are Next.js failures rather than generic ones, so a generic checklist misses several of them entirely.

This list is ordered by what somebody finds first, and it assumes you have exported the project or connected it to a repository. Work down it and stop at the first thing that fails, because a confirmed authorisation bypass outranks everything under it.

1. What Next.js version are you actually on?

Why first: CVE-2025-29927 let anyone bypass middleware entirely by sending an x-middleware-subrequest header. If your authorisation lived in middleware, and on a generated project it usually does, that flaw handed out every protected page to anyone who read the advisory.

How to check: open package.json and read the pinned version. You need 15.2.3 or later on the 15 line, 14.2.25 on 14, 13.5.9 on 13, or 12.3.5 on 12.

A failure looks like: anything below those, including a caret range that has never been reinstalled. Vercel patched apps hosted on their platform, so an older pin is mostly a problem for a project you host elsewhere, and "mostly" is doing work in that sentence.

2. Is middleware the only thing standing between a stranger and your data?

Why it matters beyond that CVE: middleware is an edge function that runs before your route, and it is the wrong place to hold your only authorisation check. It is easy to skip, easy to misconfigure with a matcher that does not cover a path, and it cannot see what a specific record belongs to.

How to check: open middleware.ts and read the matcher. Then open two or three protected route handlers and see whether any of them check the session themselves.

A failure looks like: a route handler that reads a record and returns it with no session check of its own. Middleware is a filter you put in front for convenience, not the wall.

3. Does every server action check who called it?

Why v0 specifically: a server action is a public HTTP endpoint with a nice function signature on top. It looks like a local function call, which is exactly why people forget it can be invoked directly by anyone who has the page.

How to check: search the project for "use server" and read every function under it. Each one needs to establish who is calling before it touches data.

A failure looks like: an action that takes an id and updates it, with no line that resolves the current user and compares. deletePost(id) with no owner check is a delete-anything endpoint.

4. Did a secret cross into a client component?

How to check: search for NEXT_PUBLIC_ and read what each one holds, then search for "use client" and check whether any file under it imports something that reads a secret. Finally, load the deployed site, open developer tools, and search all files for sk-, secret and service_role.

A failure looks like: an API key in a NEXT_PUBLIC_ variable, or a helper that reads process.env being imported from a client component. The build will not warn you; it will inline the value and ship it.

5. Can a second account read the first one's records?

How to check: create two accounts. As A, note a URL or an API call carrying an id. As B, request the same thing, both in the address bar and in the network tab.

A failure looks like: account A's data rendering for account B. This is the most common serious flaw in generated apps and it is invisible while you test as yourself, because as yourself every check passes.

6. Is RLS on, and were the grants revoked?

Why both: if you wired v0 to Supabase, row-level security is off by default on new tables, and switching it on does not revoke the privileges the table was created with. That second step is documented by Supabase and almost nobody runs it.

How to check: read the RLS column in the table list, then run revoke all on table <name> from anon, authenticated; and grant back only what the app uses.

A failure looks like: a table marked disabled, or one marked enabled that still answers an anonymous query.

7. Does your paywall exist on the server?

How to check: as a free account, find the request a paid feature makes and replay it from the network tab. Then delete a display:none in the elements panel and see whether the premium view renders with real data.

A failure looks like: the request answering. A conditional render is presentation, and presentation does not survive somebody opening developer tools.

8. Is anything you render as HTML actually trusted?

How to check: search for dangerouslySetInnerHTML and read where each value came from. Then put <script>alert(1)</script> into a field another user will see and view it from the second account.

A failure looks like: an alert box, or a dangerouslySetInnerHTML fed by anything a user typed. React escapes output for you everywhere except that one prop, which is why it is spelled that way.

9. Do your route handlers validate their input?

How to check: pick a POST handler and send it a body with an extra field, a wrong type and a missing required key. Send a string where a number belongs.

A failure looks like: a 500 with a stack trace, or worse, a 200 that wrote the extra field straight into the record. Mass assignment through a spread into an update is a normal generated pattern and it lets a caller set columns you never exposed.

What nine checks still leave open

These cover the classes that are common enough to name. They do not touch your business logic, and that is where the expensive failures live: a coupon that applies twice, a total computed in the browser and trusted on the way back, a webhook handler that never verifies its signature, an upload path assembled from a filename.

Finding those means reading the code, and a repository has more surface than anyone reads in an evening. That is what Sentrint is for: it reads the whole project, tells you what an attacker would reach first in plain English, and gives you a fix you can paste back into v0. It is a code scan rather than a pentest, so it reads what you wrote instead of attacking your running site, and the first scan is free with no card.

Start with the version pin and the server actions. Those two are quick, and they are where a v0 project differs from every other generated app on the internet.