We scanned 292 Next.js apps. One in five can delete your data without asking who you are.
Forty per cent of what we found was not in an API route. It was in files whose names promise a library and deliver an endpoint.
There is a function in a real, public, actively maintained codebase that looks like this:
/**
* Deletes expired status reports and their associated video files.
* Called by the cleanup cron API route.
*/
export async function cleanupExpiredReports() {
const now = new Date();
const expiredReports = await db.statusReport.findMany({
where: { expiresAt: { lt: now } },
});
...
}
It lives four directories deep, under features/status-reports/Actions/.
It takes no arguments. Its own comment tells you what calls it: the cron route.
Every signal in that file says internal plumbing, nobody touches this but
the scheduler.
The first line of the file is 'use server'.
Which means it is not internal. It is an HTTP endpoint. Next.js turns every
export of a 'use server' file into a POST endpoint addressed by a
generated action ID. Anyone who can discover that ID can invoke the function
directly — which is why Next.js's own documentation says server actions must
perform their own authorization checks. This one performs none, because the
author never imagined anyone would be asking.
I do not think that developer was careless. I think they wrote a helper, put it somewhere sensible, and had no reason to look at line 1.
The thing worth being scared of is not that people write bad code. It is that the surface of a modern application is no longer visible by reading it.
What we measured
We took 400 public Next.js repositories from GitHub, sampled across four star bands so the result would not simply describe famous projects, and scanned every one with a static analyser that reads code without running it.
292 scanned cleanly — 23,120 ways in. Pages, API routes, server actions. Every entry point, what data each one touches, and whether anything establishes who the caller is before it runs.
141 of those applications can do something consequential: delete data, take payment, or change who has access. Those are the only ones that can fail this test — an application that only reads things cannot leak your database.
22.7%
Of the 141 applications able to fail, 32 had at least one consequential endpoint with no visible check on who is asking. 95% confidence interval 16.6–30.3. Counting findings the scanner itself flagged as uncertain, 27%.
The part that should worry you
124 findings in total. Sixty per cent were in route.ts files —
ordinary API routes, where at least you know you have built a door.
Forty per cent were server actions. And of those, a third were
not in a folder called actions/. They were in db/,
services/, local/, google-drive/. Files
whose names promise a library and deliver an endpoint.
One of them exports deleteGoogleDrive(config, input). It resolves a
file path and calls client.files.delete(). It reads exactly like a
storage adapter — the kind of function you would expect three layers of
application code to sit in front of. It is directly reachable over HTTP.
You cannot catch this by reviewing a pull request, because the diff looks fine. You cannot catch it by reading the file, because the file looks like a library. You catch it by knowing that the first line changes the meaning of every export below it — and by checking, every time, on every file, forever.
Nobody does that. It is a job for a machine.
What we got wrong
Measuring 292 codebases we did not write broke our own scanner in four ways. It would be dishonest to publish the number without the list.
- It discarded monorepos. Thirteen of the first forty-eight projects were workspaces, and the scanner recorded them as unreadable rather than following its own hint to the application inside. That is 27% of a sample, and the excluded projects were the substantial ones. Any figure produced before that fix was quietly biased toward toy apps.
- It flagged the front door. Registration endpoints and one-time-code senders were reported as having no check on who is asking. True, and useless: those cannot sit behind a check, because they are what you use before you have an identity. 5.3% of findings, now suppressed.
- It dropped its own uncertainty. Every finding carries a confidence — whether we could see the whole path, or something blocked the view. The report always showed it; the JSON output silently threw it away, so the CI integration built on that output could not tell a firm finding from a hedged one. We found that only because this study needed the field and it was not there.
- Our precision claim was never really tested. We had been proud of scanning a 4,000-file monorepo and reporting nothing. But a tool that finds nothing has a perfect false-positive rate and has proved nothing at all. This is the first time the rule fired at scale on code we did not write. Twelve findings hand-checked, twelve correctly identified — a spot check, not an audit, and we will keep saying so until it is one.
Zero crashes across 400 repositories, at least.
What this is not
It is not a claim that one in five Next.js applications is exploitable. We measured missing visible checks, not proven holes. A check can live in row-level security, in a middleware matcher we could not resolve, in an import we could not follow. Thirteen of the 124 findings say exactly that, in the finding itself.
It is not a claim about AI-written code. Nothing in a repository tells you who wrote it, and we are not going to invent that.
And no repository is named here, or anywhere. These are findings to check, not proven vulnerabilities, and publishing a list would be handing out a target sheet for a claim we have not made.
The band breakdown ran from 16% in the least-starred projects to 32% in the most. Do not read a trend into that: every confidence interval overlaps every other, and larger projects simply have more endpoints, so more chances to hold one.
Go and look at yours
The scanner is free, MIT licensed, and makes no network requests at all. It reads your code on your machine and writes a single HTML file next to it. No account, nothing uploaded, no telemetry — disconnect from the network and it works unchanged, which is the honest way to check that claim rather than taking our word for it.
npx what-it-does
It took ten and a half seconds on a 4,000-file monorepo, and tells you every way into your application, what each one touches, and where nothing is checking who is asking. If it finds nothing, you have learned something real — it stayed quiet on all 4,000 of those files, because there was nothing there to report.
The method, the sampling frame and the harness are published on GitHub, so you can re-run this and disagree with us.
If you would rather not have to remember, there is a paid GitHub Action that runs the same comparison on every pull request and comments only when your application's behaviour actually changed. Free on public repositories. The scanner is the part that matters, and it costs nothing.
One thing I would ask. If it tells you something wrong about your own code, send it to [email protected]. That is what the accuracy work runs on, and this study is what happens when it finally gets some.