Cloudflare's AI wrote vinext for $1,100. We audited it for $200.
Eleven days after vinext's first commit, our $200 automated audit of Cloudflare's AI-written Next.js framework found a cross-user session leak, fetch cache poisoning, and a middleware bypass. Cloudflare paid $4,600 and fixed all five reported findings.

Dragoș Albăstroiu
August 17, 2026
On February 13, 2026, Cloudflare landed the first commit of vinext, a reimplementation of the Next.js API surface on Vite that deploys to Cloudflare Workers with one command. One engineering manager directed the work.
In Cloudflare's own words, "almost every line of code in vinext was written by AI," and the whole thing "cost about $1,100 in Claude API tokens." The project was under a week old when they published.
Eleven days after that first commit, we pointed our automated source-review pipeline at the repository. The audit ran for 4 hours and 12 minutes across 840 files and roughly 60,000 lines of TypeScript, and returned 22 findings.
We selected the five that clear a bounty's bar, built working proof of concept exploits for each, and reported them to Cloudflare's public bug bounty on February 26. Cloudflare accepted all five, paid $4,600, and has since fixed every one of them.
At today's pricing, that audit costs $200. That is list price, what a customer pays us, not our inference bill.
The five reported vulnerabilities
| Finding | Cloudflare severity | Bounty |
|---|---|---|
Session cookie leakage via unsafe AsyncLocalStorage fallback | High | $1,000 |
| Fetch cache key omits auth headers, leaking responses cross-user | High | $1,000 |
Default middleware matcher silently excludes /api/* | High | $1,000 |
Unvalidated dynamic import via crafted __NEXT_DATA__ | High | $850 |
| Open redirect and SSRF path traversal on catch-all parameters | Medium | $750 |
| $4,600 |
Finding these bugs cost less than a twentieth of what it cost to reward them.
Where Next.js compatibility layers break
vinext is not an application. It is a framework that promises Next.js semantics on a different runtime: Vite instead of Turbopack for the build, Cloudflare Workers instead of Node for the server. Cloudflare reports 94% coverage of the Next.js 16 API surface, over 1,700 unit tests, and 380 Playwright end-to-end tests.
That test suite answers one question well: does the reimplementation behave like Next.js? It does not answer the question we care about: where the reimplementation quietly does not behave like Next.js, does anything unsafe follow?
This is the structural weakness of any compatibility layer, and it does not depend on who or what wrote the code. Developers migrate their mental model along with their application. They keep the security assumptions they learned from the original, and those assumptions stop holding wherever the copy diverges.
A passing test suite cannot detect that, because both sides of the divergence are working as their authors intended.
All five findings we reported are divergences of that kind. That is also how we proved them: we deployed the same application twice, once as Next.js on Vercel and once as vinext on Workers, and used Next.js as the control.
The claim is never "this code looks wrong." It is "the framework it replaces does not do this." Next.js is the control for behavior, not a proof of safety; it has App Router bugs of its own.
The cross-user session leak: AsyncLocalStorage on Cloudflare Workers
Next.js isolates per-request state with AsyncLocalStorage. vinext did the same, using AsyncLocalStorage.enterWith() to attach the current request's headers and cookies to the async context. Cloudflare Workers does not support enterWith(). When the call failed, vinext fell back to a module-level variable:
1// packages/vinext/src/shims/headers.ts, at the time of the audit 2_fallbackState.headersContext = ctx;
A module-level variable in a Workers isolate is shared by every request that isolate is currently handling. The reader had no way to tell it was reading the wrong request's data:
1function _getState(): VinextHeadersShimState { 2 const state = _als.getStore(); 3 return state ?? _fallbackState; 4}
Because enterWith() failed and no als.run() scope was ever established, getStore() returned null and every read fell through to the shared object.
So while Alice's request was suspended at an await, Bob's request overwrote _fallbackState.headersContext with his own cookies. When Alice's render resumed and called cookies(), it returned Bob's session.
The code documented the risk it was taking, with a comment noting the fallback was "not concurrency-safe but works for dev." That was true on Node, where enterWith() works and the fallback is dead code. On Workers, the fallback was the only path.
Reading the code gives you the bug. We wanted the rate. So we deployed a page that calls cookies(), fired 20 concurrent requests alternating between two sessions, and counted how often the response came back with the wrong one:
| Deployment | Responses with the wrong user's session |
|---|---|
| vinext 0.0.8 on Cloudflare Workers | 3% to 6% |
| Next.js 16.1.6 on Vercel, same application | 0% |
Ordinary concurrent traffic was the exploit.
Fetch cache poisoning: a key that ignored who was asking
The fetch cache was an independent bug with the same effect. vinext built its cache key from the method and URL alone, and deliberately dropped per-user headers:
1// packages/vinext/src/shims/fetch-cache.ts, at the time of the audit 2const parts = [`fetch:${method}:${url}`]; // Authorization and Cookie NOT included
An authenticated fetch() inside a Server Component with cache: 'force-cache' therefore stored the first user's private response under a key every other user also computed. Alice loaded the page, and Bob saw Alice's data.
With the KV-backed cache handler that response persisted globally and was replayed to everyone, including unauthenticated visitors, until the entry expired.
The same broken assumption, four times over
The enterWith() fallback was not confined to headers.ts. It turned up in three further places, which we did not report separately because they leak less than a session cookie:
| Where | What crossed between requests |
|---|---|
navigation-state.ts and router-state.ts | Pathname, query parameters and locale |
| Pages Router Worker entry | The import that installs per-request isolation for <Head> was missing, so personalized page titles could cross between users |
One wrong belief about one runtime primitive, copied into four places, because nothing in the process that wrote the code ever went back to check it.
Middleware bypass: the default matcher that skipped /api
Next.js runs middleware on every path when no matcher is configured. vinext's default matcher did not:
1// packages/vinext/src/server/middleware.ts, at the time of the audit 2return ( 3 !pathname.startsWith("/_next") && 4 !pathname.startsWith("/api") && // all API routes skip middleware 5 !pathname.includes(".") && // /admin.config, /dashboard.old, ... 6 pathname !== "/favicon.ico" 7);
An engineer who moved a Next.js application to vinext and relied on middleware for authentication had unprotected /api/* routes, and any dot-containing path bypassed middleware as well.
Nothing warned them: the middleware still ran, and no longer covered what they believed it covered. Side by side against the Vercel control, the same request returned 401 on Next.js and the secret payload on vinext.
Open redirect and SSRF from a single decode
The fourth finding started as an open redirect and turned out to be worse. vinext decoded catch-all path segments before substituting them into redirect and rewrite destinations, so %2F became a real slash after the guard had already inspected the raw path, and /old/%2Fevil.com produced Location: //evil.com.
The same decode sat in front of rewrites, and when a rewrite points at an external origin, ..%2f escaped the intended path prefix on the upstream server. vinext's proxy forwards the original request headers, so the traversed request carried the victim's Cookie and Authorization to whatever path the attacker reached.
A validator that guarded one path and not the other
The fifth finding is the smallest. During client-side navigation, vinext read a module URL out of the __NEXT_DATA__ JSON embedded in the fetched page and handed it straight to import().
The hydration path guarded that same value with an isValidModulePath() check that rejects external URLs and traversal. The navigation path did not call it.
So anyone who could influence that JSON, through stored HTML or a user-controlled link target, could have a victim's browser import and run script from an arbitrary origin.
How Cloudflare fixed them
All five are remediated on main. We went back and read the current source rather than taking the report statuses at face value:
| Finding | Fix |
|---|---|
| Session cookie leakage | enterWith() is gone. The request path now establishes a real scope with await _als.run(state, fn). |
| Fetch cache | Headers are part of the cache key, with only traceparent and tracestate excluded. |
| Middleware matcher | matchesMiddleware now returns true when no matcher is configured, matching Next.js exactly. |
| Unvalidated dynamic import | isValidModulePath() now guards the navigation path, falling back to a hard navigation on a bad path. |
| Open redirect and SSRF | Fixed across several pull requests, with further encoded-segment hardening later on. |
The fetch cache fix is better than what we recommended. Alongside putting headers in the key, they added a rule we did not ask for: when per-user auth headers are present and the developer has not explicitly opted into caching, the response is not cached at all.
Our report offered including a header hash or defaulting to no-store as alternatives. They did both, and the second one protects developers who never read either document.
What a bounty pays for
A bug bounty pays for one thing: what an attacker can demonstrably do to a system in scope. That bar is high and narrow on purpose, and it is not the question an audit report answers. The report carries what a defender should act on, which includes the issues that need a precondition and the quiet deviations from secure defaults.
The full report is 2 critical, 2 high, 8 medium and 10 low: fifteen written up as vulnerabilities, seven as observations. Five of those clear a bounty. Both numbers are right, for different questions.
The whole thing is published as the platform generated it: AISafe vinext Source Code Audit, 42 pages, PDF. Every finding carries proof of concept code and remediation, so you can judge the other seventeen yourself.
Cloudflare's triage is a second opinion on the five we sent, and two of its calls are worth repeating.
They rated our two criticals as high. On the cookie leak their reasoning was explicit: exploitation "requires specific concurrent load and isolate degradation conditions," it grants no code execution, and vinext is an experimental repository with limited deployment. That is a fair read, and we would rather quote it than argue with it in a post they cannot reply to.
They already knew about the unvalidated dynamic import, and paid the $850 anyway, as they put it, "as a gesture of appreciation." Worth saying out loud, because it is $850 of the $4,600 in the headline.
The new economics of AI-written code
Here is the whole engagement in four numbers.
| Cost | |
|---|---|
| Building vinext (Cloudflare, AI-assisted) | ~$1,100 |
| Auditing vinext (AISafe, automated) | $200 |
| Bounties Cloudflare paid for five findings | $4,600 |
| A traditional pentest of a 60k-line codebase | $10,000 to $20,000 |
Cloudflare produced a working framework in six days for about $1,100 because a model did the typing. Every team with an API key can now do that.
More software will arrive faster from smaller teams, and much of it will fail the way vinext did: correct against its own tests, wrong where it meets its runtime.
Security review priced and paced like consulting cannot meet that. Scoping a manual review of a 60,000-line framework puts you in five figures and several weeks of calendar time, and vinext did not exist several weeks before we audited it. Code written in six days cannot wait six weeks to be reviewed, and a review that costs ten to twenty times what the code cost to write does not get bought.
So it does not happen. The alternative to an expensive review is not a cheaper one, it is no review, and a bill that arrives as an incident rather than as dollars per finding.
An audit that runs in hours for $200 can meet it. This one found a reproducible cross-user session leak in a codebase with 1,700 passing tests, eleven days after the code first existed. It is not a one-off: the same pipeline found two command injection CVEs in Warp Terminal shortly after Warp open-sourced its client.
Cloudflare fixed all five and improved on one of our recommendations while doing it. Eleven days from first commit to reported and verified findings is the cadence this code now demands, because it is the cadence the code was written at.
