CodeVetter · Repo Unpack · Risk map
vercel/next.js
The biggest blast-radius surfaces are the header-filtering allowlist, the dev-server origin guard, the Server Actions CSRF check, the image optimizer's SSRF path, and the PR-adoption script that grants contributor code access to CI secrets. Structurally, `app-render.tsx` (10,768 lines), `build/index.ts` (4,815) and `base-server.ts` (3,459) are single files that concentrate enormous behaviour and are the natural home of regressions. There is also a large amount of implicit coupling via global symbols, `process.env.__NEXT_*` compile-time constants, and an env-var-driven test matrix, all of which make 'works locally' a weak signal unless the exact CI env is mirrored.
- `INTERNAL_HEADERS` is a hardcoded allowlist, not a prefix rule — any new internal header (e.g. a future `x-middleware-*`) that is read by server code but not added to this array is forgeable by an external client, and the repo's own AGENTS.md flags this as a mandatory PR review check.
- `filterInternalHeaders` is skipped entirely when `NEXT_PRIVATE_TEST_HEADERS` is set; anything that leaks that env var into a non-test process disables internal-header sanitisation for the whole server.
- `blockCrossSiteDEV` only guards URLs matching `/_next` or `/__nextjs` and explicitly excludes `/_next/image`, `/_next/static/media`, and `/_next/static/immutable/media` from the check, so those three paths are reachable cross-site in dev by design — a deliberate CORS tradeoff worth re-checking whenever media handling changes.
- The dev MCP server at `/_next/mcp` executes agent tools (`get-logs`, `get-server-action-by-id`, `compile-route`) and is protected only by virtue of sitting under the `/_next` prefix that `blockCrossSiteDEV` covers; it parses request bodies with a 1MB cap and swallows every error into a generic JSON-RPC 500.
- Server Actions CSRF hinges on `x-forwarded-host` being trustworthy: `getIsPossibleServerAction`-adjacent logic prefers `x-forwarded-host` over `host` when comparing to `Origin`, so a misconfigured reverse proxy that lets clients set that header weakens the check.
- SSRF protection in the image optimizer is DNS-resolution-based and opt-out: `fetchExternalImage` resolves the hostname and rejects private IPs via `isPrivateIp`, but `images.dangerouslyAllowLocalIP = true` disables it, and the resolve-then-fetch pattern is inherently TOCTOU against DNS rebinding.
- `matchWildcardDomain` in `csrf-protection.ts` is a hand-rolled glob matcher written specifically because micromatch cannot run at the edge; it carries explicit special-cases (rejecting bare `*`/`**`, rejecting `**` in non-terminal position) and is the shared basis for both `serverActions.allowedOrigins` and `allowedDevOrigins`, so a bug there affects two security checks at once.
- `pnpm pr-adopt` (`scripts/adopt-pr.js`) re-pushes contributor commits into `vercel/next.js`, which grants that code access to repository secrets during install/build/test; it defends with an interactive confirmation naming the author and head SHA, a throwaway worktree, and `core.hooksPath` redirected to an empty directory via `GIT_CONFIG_COUNT` to neutralise `.husky/*` hooks.
- `app-render.tsx` is 10,768 lines and holds the entire RSC render, prospective/final prerender, and resume-data-cache logic in one module — the single highest blast-radius file in the repo.
- Cache handlers are wired through nine `Symbol.for('@next/cache-handlers*')` globals rather than imports; because they live on `globalThis`, two copies of the module share state, and a stale symbol contract between the framework and an external handler package fails silently rather than at type-check time.
- Fragile coupling: `renderServerOpts.serverFields.routerServerHandler = requestHandlerImpl` creates a cycle where the render server calls back into the router server's handler, which is what makes the `handleIndex > 5` recursion guard necessary.
- Deprecated-but-live config surface is a trap: `experimental.ppr`, `experimental.cacheComponents`, and `images.domains` are all marked deprecated yet still read at runtime, so code must handle both the old and new location of the same setting.
- `NextServer` retains five deprecated methods that all emit the same 'Please use `app.getRequestHandler()` with an adjusted parsed URL instead.' warning — legacy API surface that still has to keep working for the programmatic-server use case.
- Compile-time env constants are the main source of 'passes locally, fails in CI': `process.env.NODE_ENV !== 'production'` and `process.env.__NEXT_DEV_SERVER` mean different things (`next build --debug-prerender` produces a development bundle without a dev server), and snapshot output differs between Turbopack (`react-dom/server.edge`) and webpack (`react-dom/server.node`).
- Untested-by-construction flow: the dev memory-threshold restart path exits the process from inside a request's `finally` block, so any in-flight streaming responses are dropped; it is triggered by real heap pressure and therefore hard to cover in the integration suite. (inferred)
- The `patches/` directory carries 11 pnpm patches against third-party packages (`@rspack/core@1.6.7`, `playwright-core@1.61.0`, `@types/node@20.17.7`, `@modelcontextprotocol/sdk`, `webpack-sources`, `taskr`, …), each of which must be re-verified on every upstream bump.