A security audit for a v0 app, checked at the Next.js boundaries
The coding agent writes the software. StackAttest independently validates the software that was written. In a Next.js application that mostly means checking boundaries: which code runs on the server, which of it ships to the browser, and which of the two the application is trusting when it decides whether a request is allowed.
Where the boundaries are checked
The boundaries live in the source, so repository validation carries most of this page. Running the application is what establishes whether the boundary holds for a request that did not come from your interface.
Repository validation
E2Reads the source: where secrets live, whether authorisation is enforced on the server or only in the interface, whether data access is injection safe, and which dependencies carry known critical vulnerabilities. Findings cite the file they came from, so a result is checkable rather than asserted.
Runtime validation
E4Builds and runs the application in an isolated environment and probes the running system. This is the layer that answers questions reading code cannot: whether the deployed configuration actually refuses an unauthorised request, and whether error responses leak internals under real conditions.
Public URL scan
E4Tests the deployed application from outside, the way any visitor reaches it: transport security, security response headers, and what the running service discloses about itself. It needs nothing but the address, and what it observes it observes against the running system. Its limit is reach rather than strength: a URL can never establish authorisation, data-layer or backup posture, and those controls stay uncovered.
The Next.js boundaries an audit has to check
These are failure modes common in the architecture, not claims about your application. Each is a place where an app can look correct in the browser and behave differently when the request does not come from the browser. These are failure modes common to this architecture, not a claim about your application, and the audit establishes which of them are actually present.
A Server Action is a public HTTP endpoint
Marking a function with the "use server" directive publishes it. The framework gives it an identifier and the browser posts to that identifier, so anyone can post to it too. Which component calls the action, and whatever that component checked before calling it, constrains nothing. Every action needs its own authentication check, its own authorisation check and its own validation of the arguments it receives, exactly like a route written by hand.
Middleware is not an authorisation boundary
Middleware is the right place to send a signed-out visitor to the login page. It is the wrong place to decide whether this user may act on this record: it runs before the handler, without the data the decision needs, and it only covers the paths its matcher happens to match. One route added outside the matcher is not protected, and nothing about that is visible in the interface. The check that counts belongs next to the data access.
Route Handlers that trust the identifier they are given
A route file under the API directory is as public as any endpoint. Reading an identifier from the request and acting on it without checking who owns the record is the most exploited weakness in the OWASP API list, and it survives testing because the interface only ever sends the identifier it should.
The NEXT_PUBLIC_ prefix is a decision to publish
Any variable whose name starts with NEXT_PUBLIC_ is inlined into the browser bundle when the app is built. It is the most common way a secret escapes a Next.js application, and it usually arrives as a rename rather than a misjudgement: the value was needed in a component, the prefix made the error go away, and the key shipped to every visitor. Removing the prefix later does not recall the bundles already downloaded.
The client and server split, one import away
The "use client" directive moves a file and everything it imports into the browser bundle. A helper that reads a secret or holds a privileged database client is safe in a server component and exposed in a client one, and the import that crosses the line is usually two files away from anything a person reviewed. Importing the server-only package into a module that must never ship turns that mistake into a build failure instead of a deployment, and the audit checks whether the modules that need it have it.
Webhook handlers that accept what they are sent
A handler receiving payment events has to verify the signature against the raw request body, and it has to stay correct when the same event arrives twice. Generated handlers frequently parse the payload and act on it, which means anyone who learns the address can announce a payment that never happened. Stripe is the usual case and the expensive one.
What the result looks like
An illustrative extract, not a real customer result. Every row carries the evidence grade behind it, and a control nobody could establish says so.
| Control | Status | Evidence | Note |
|---|---|---|---|
| Object ownership enforced server-side | Failed | E2 | Two Server Actions act on a caller-supplied identifier |
| Secrets managed outside source code | Failed | E2 | A privileged key is named with the NEXT_PUBLIC_ prefix |
| Server-side input validation on state-changing endpoints | Partial | E2 | Validated in the form, not in the action it posts to |
| Error responses do not leak internals | Verified | E4 | Stack traces suppressed under induced failures |
| Health checks and basic observability | Uncovered | Not graded | No evidence available from the layers that ran |
Mapped to published standards
Each control is tested against a named clause, so a result means something outside our own vocabulary. Mapping is not certification, and none of these bodies endorse StackAttest.
What this audit is not
- StackAttest is not affiliated with Vercel and does not integrate with it. There is no access to your account, your deployments or your project settings; the audit works from what you give it.
- It is not a penetration test. A deterministic check and a runtime probe are not an adversarial human engagement, and there are classes of weakness only a person hunting for them will find.
- It is not SOC 2 and does not replace it. SOC 2 attests to organisational controls over time; this validates technical controls in the software. They answer different questions for different buyers.
- It does not replace human due diligence. It gives a reviewer repeatable evidence to start from, which is a different thing from being the reviewer.
- Controls the run could not reach are reported as uncovered rather than passed. A result you did not earn is worse than no result.
- It is not a Vercel integration. There is no connection to your Vercel account, your deployments or your project settings. The audit works from the address you give it, the repository you connect, and the application it builds and runs in isolation.
Questions
What is a v0 security audit?
An independent check of an application generated with v0, tested against named controls and focused on the boundaries a Next.js app is judged on: what runs on the server, what ships to the browser, and where the decision to allow a request is actually made. Every result records the strength of the evidence behind it.
Do you connect to my Vercel account?
No. There is no Vercel integration, no access to your deployments and no read of your project settings. You give us a public address, or connect a repository read-only, and the audit works from those.
Are Server Actions safe by default?
They are convenient by default. A Server Action is a public endpoint the framework routes for you, so the arguments arrive from the network rather than from your component, and the caller is whoever posts to it. The controls that apply to any endpoint apply here, and the audit checks whether they are present in each one.
Is middleware enough to protect my routes?
It is enough to redirect a signed-out visitor, which is what it is good at. It is not enough to decide whether a signed-in user may act on a particular record, and a route outside its matcher does not run it at all. Treat it as routing with a login redirect, and put the decision that matters next to the data.
How do I know if a key leaked into the browser bundle?
That is one of the things repository validation looks for: which values carry the NEXT_PUBLIC_ prefix, and whether any of them is a credential that would bypass your access rules. If one is, the finding cites the file, and the first remediation step is rotating the key rather than deleting the line.
Can you test the deployed app without the source?
Yes. A URL scan tests the running deployment from outside and runtime validation probes its behaviour, and both produce evidence about the deployed system rather than the code. The report is explicit about which layers ran, and controls they could not reach are listed as uncovered.
Related
Find out what is actually true of your application
Start with the layer you can run today. The report says which layers ran and what they could not reach, so the result is honest about its own limits.