Assay
Blog
Audit2026-03-15

We Ran Assay on Next.js. 601 Claims. 90 Bugs. 17 Failures.

6 core server modules. 210 source files. 601 claims extracted and verified. 90 bugs. 17 confirmed failures.

We pointed Assay at Next.js canary and scanned the server-side internals. Not the docs. Not the tests. The actual runtime code that runs on every request.

ModuleScoreClaimsBugsFails
router-utils88/100132140
stream-utils86/10015482
app-render83/100116150
route-modules73/10049112
use-cache73/10069163
server/web63/100812610
Total78 avg6019017

Average score: 78/100. The routing layer is solid. The caching and edge runtime are where the problems live.

Top findings

1. unstable_cache serialization has no fallback

JSON.stringify(result) on line 44 of unstable-cache.ts has no try-catch. Circular references, BigInt, or functions crash the whole request. The TODO comment confirms the team knows.

// unstable-cache.ts:44
body: JSON.stringify(result),  // no try-catch

No fallback to uncached execution. A non-serializable return value takes down the request.

2. respondWith is dead code in edge runtime

The base FetchEvent class implements respondWith() which stores a response promise in a private Symbol. NextFetchEvent overrides it to throw. The stored promise is never read anywhere.

// fetch-event.ts:25-30
// TODO: is this dead code? NextFetchEvent never lets this get called
respondWith(response: Response | Promise<Response>): void {
  if (!this[responseSymbol]) {
    this[responseSymbol] = Promise.resolve(response)
  }
}

Not a security issue. But the edge runtime's FetchEvent does not conform to the Service Worker spec it claims to implement.

3. ImageResponse in next/server is a throwing stub

Moved to next/og in Next.js 14. The old export still exists but throws immediately. 5 FAIL verdicts from one stub. Any code importing from the old location gets a runtime crash, not a build-time error.

// image-response.ts
export function ImageResponse(): never {
  throw new Error(
    'ImageResponse moved from "next/server" to "next/og" since Next.js 14...'
  )
}

4. Edge preview mode reads env vars, not cookies

getEdgePreviewProps() claims to resolve preview mode from request cookies. It reads process.env directly. No cookie parsing. No validation. The function name implies request-scoped behavior. The implementation is global.

// get-edge-preview-props.ts
export function getEdgePreviewProps() {
  return {
    previewModeId: process.env.__NEXT_PREVIEW_MODE_ID || '',
    previewModeSigningKey: process.env.__NEXT_PREVIEW_MODE_SIGNING_KEY || '',
    previewModeEncryptionKey: process.env.__NEXT_PREVIEW_MODE_ENCRYPTION_KEY || '',
  }
}

Cannot distinguish between preview and non-preview requests.

5. CSRF protection exists but enforcement is outside the render flow

isCsrfOriginAllowed() is implemented with 28 test cases. But the render flow does not call it. Enforcement happens at the router layer, not the render layer. A direct invocation of the render pipeline bypassing the router would skip CSRF checks.

What we didn't find

router-utils scored 88/100. Zero FAILs. Cross-site request blocking, URL decoding, WebSocket proxying, and build-time cache type generation all hit 100%.

stream-utils scored 86/100. Core stream operations all passed. Risk is in the HTML transformation layer. The byte-level utilities are clean.

app-render scored 83/100. Route Tree Prefetch, Metadata-Only Navigation Response, Server Action Encryption Key Management, and Segment Data Collection all achieved 100%. The happy-path rendering pipeline is reliable.

Methodology

ToolAssay v0.32.0
TargetNext.js canary (2026-03-15)
Modules6 server-side modules
Source files210 (.ts/.tsx/.js)
Claims verified601
Time~2 hours

No modifications to Next.js code. Read-only analysis. Some large files were truncated by context window limits, which may cause false negatives in the use-cache and stream-utils modules.

Try it yourself

Run it on your project:

npx tryassay assess .

90 seconds. You get a score, a list of every claim that failed, and what to fix.

Drop a repo link. I'll run it for free.

- Ty