Commit Graph

16 Commits

Author SHA1 Message Date
Xplod13 5e37596183 fix(routing): re-encode trailing-slash Location for non-Latin-1 paths (#2011)
normalizeTrailingSlash built the redirect Location from the already
percent-decoded pathname. A character above U+00FF (e.g. a CJK slug or
emoji under trailingSlash:true) made the Headers constructor throw
'Cannot convert argument to a ByteString', caught by the outer handler
and surfaced as a 500 instead of a 308. Latin-1 characters such as
spaces did not throw but produced a malformed, un-percent-encoded
Location.

Percent-encode the redirect target before building the response,
encoding every character that is invalid raw in an RFC 3986 path while
preserving existing %xx escapes and / delimiters so already-encoded
delimiters like %23 are not double-encoded.

Fixes #1979

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 19:18:25 +01:00
James Anderson bf41ee49dd fix(image): prevent optimizer cache-key amplification (#1944)
* fix(image): validate optimizer parameters

* fix(image): close optimizer amplification aliases

* fix(image): validate pure app dev requests

* fix(image): address optimizer review findings
2026-06-13 01:10:12 +01:00
James Anderson c023fc3810 fix(actions): bound Flight payload graph validation (#1942) 2026-06-13 00:56:57 +01:00
Nathan Nguyen dab28c4b0f fix(app-router): refetch same-page search navigations (#1744)
* fix(app-router): refetch same-page search navigations

Same-page App Router navigations with a changed search string could replay a prefetched RSC payload instead of requesting the target payload. That diverges from Next.js, which refetches page segments for same-page navigations because search params are page inputs.

Bypass visited and prefetched RSC caches for navigate operations whose pathname is unchanged but search params differ. Add a prerendered production browser regression that ports the Next.js hash navigation request assertion.

* fix(app-router): hide rsc cache query from middleware

* test(app-router): mirror upstream hash scroll offsets

* fix(app-router): address review feedback on rsc query navigation

- Reuse fixture-scoped node_modules linking in the hash RSC spec instead of
  symlinking repo-root node_modules, matching sibling production specs.
- Add an explicit assertion that no request leaked `_rsc` to the fixture
  middleware (HTTP 599) for a clear failure mode.
- Document that the optimistic shell is intentionally not gated by
  shouldBypassNavigationCache (detached commit, always superseded).
- Detect the RSC cache-busting param via the encoding-aware stripper so the
  guard and stripper can never diverge on encoded-key edge cases.

* fix(app-router): share encoding-aware matcher for _rsc guard

Replace the before/after url.search comparison in
requestWithoutRscCacheBustingSearchParam with a dedicated
hasRscCacheBustingSearchParam helper that reuses the stripper's
isRscCacheBustingSearchPair matcher. This makes the guard and stripper share a
single source of truth and avoids spuriously rebuilding/normalizing requests
whose only difference is degenerate empty query pairs (e.g. ?a=1&&b=2).

* fix(app-router): preserve cf metadata when hiding _rsc from middleware

requestWithoutRscCacheBustingSearchParam reconstructed the userland request
with a bare new Request(url, source), which drops the Workers-specific cf
property and omits duplex: "half" for streaming bodies (throwing on
Node/undici). Add a cloneRequestWithUrl helper mirroring cloneRequestWithHeaders
that overrides the URL while preserving headers/cf and setting duplex, and route
the _rsc strip through it.

* fix(app-router): stop _rsc leaking to external rewrite proxies

The beforeFiles/afterFiles/fallback applyRewrite calls forwarded the original
request (with _rsc) to proxyExternalRequest, which reconstructs the upstream URL
from request.url and merges its query params — leaking the internal RSC
transport query to external rewrite destinations. Pass the _rsc-stripped
userlandRequest to all three calls, consistent with the middleware invariant.

Add a regression test that points an external rewrite at a loopback server and
asserts the forwarded URL drops _rsc while preserving userland query params.

* test(app-router): wait for idle network before tracking RSC requests

The query-param link is in the viewport and gets prefetched on load. On slower
runtimes (WebKit), that prefetch could land after rscRequestUrls.clear(), so it
was misattributed to the hash-only navigations and tripped the
"no with-query-param request" assertion. Wait for networkidle before clearing,
mirroring upstream's waitForIdleNetwork() in the ported Next.js test.

---------

Co-authored-by: James <james@eli.cx>
2026-06-07 00:20:27 +00:00
Nathan Nguyen 0046e0691a fix(app-router): preserve encoded prerender params (#1597)
* fix(app-router): preserve encoded prerender params

App Router prerendered pages with encoded dynamic params could be bypassed in production and rendered again with decoded route params. This diverged from Next.js for generateStaticParams values like "sticks & stones".

The root cause was split across build and startup. The prerender render path only had the encoded URL, so normal route matching decoded the page params. After preserving encoded params for the render, production startup still seeded the ISR cache under the encoded artifact path while live requests lookup cache entries by the runtime-normalized pathname.

Carry trusted prerender params through the prerender-only request path, keep decoded params for dynamicParams validation, and seed prerendered App Router artifacts under the normalized runtime cache key while reading files from their encoded output path.

* chore(app-router): clean up prerender header internals

Reuse the shared record guard for prerender route param parsing and remove the local duplicate.

Keep Next.js INTERNAL_HEADERS as the exact upstream list, with vinext-only internal headers tracked separately but stripped by the same request pipeline. This fixes the CI unit failure while preserving the prerender header trust boundary.

Factor repeated Node-header conversion and forwarded-protocol handling in the production server to avoid local assertions and duplicate loops.

* fix(app-router): scope prerender params to route pattern

Bind the internal prerender route-param header to the route pattern that produced it so encoded render params cannot cross a rewrite boundary into a different dynamic route shape.

The App Router handler now only applies encoded prerender params when the payload routePattern matches the final matched route. Otherwise it falls back to the decoded params from normal route matching.

Tighten the header parser to validate the payload shape with Object.entries/Object.keys and add a regression for a prerendered source route rewritten to a dynamic target route with a different param name.

* fix(app-router): verify prerender params match route

Require trusted prerender route params to prove both the final route pattern and the final decoded matched params before using their encoded render values.

This prevents same-pattern rewrites from carrying stale prerender params into a different concrete route match, while keeping valid prerender renders encoded and static validation decoded.

Add handler regression coverage for same-pattern rewrites and helper coverage for catch-all array comparison.

* docs(app-router): clarify prerender route-param trust boundary

The prerender route-param readers and serializer carried implicit
coupling that read as security verification but actually delegated it.
A reader new to readTrustedPrerenderRouteParams could assume it verifies
the prerender secret, when verification happens once upstream at
prod-server's nodeToWebRequest boundary and every downstream read trusts
that. The double read-then-reattach in createAppRscHandler also depends
on the secret header surviving filterInternalHeaders, with no note of
that coupling.

Document the trust contract: secret is verified at the outer boundary,
downstream reads operate on already-trusted requests gated by
VINEXT_PRERENDER=1, and the secret header must stay out of the internal
strip list for the second read to succeed. Also document the
empty-params to null serialization contract.

No behavior change; comments only.
2026-05-27 10:55:26 +01:00
Nathan Nguyen afc549d624 fix(router): normalize trailing slash parity (#1316)
Trailing slash handling treated every non-api path the same on server redirects, so trailingSlash:true added slashes to file-looking catch-all routes and Pages Router imperative navigation skipped the client canonicalization path used by Link.

Next.js splits the invariant between route-manifest redirects and client resolveHref normalization: file-looking paths lose a trailing slash, non-file paths gain one, and queries stay attached to the canonical pathname. Share the server redirect decision across Pages Router runtimes and apply the same client helper in Router.push and Router.replace.

Adds focused regressions for catch-all dot segments, query preservation, .well-known exclusion, and Pages Router history writes.
2026-05-19 18:06:36 +01:00
Divanshu Chauhan (divkix) a85c2c1dee fix: add x-action-forwarded guard and E2E tests for server action forwarding loop (#1292)
* feat(server-action): add x-action-forwarded guard to prevent forwarding loops

Defensively implements the Next.js server action forwarding loop fix.
- Adds ACTION_FORWARDED_HEADER constant to headers.ts
- Adds x-action-forwarded to INTERNAL_HEADERS blocklist
- Adds guard to handleServerActionRscRequest and handleProgressiveServerActionRequest
- Returns 404 + x-nextjs-action-not-found when header is present

Ported from Next.js: vercel/next.js@20892dd

* test(e2e): add action-forward-loop fixtures and tests

- Adds /nextjs-compat/action-forward-loop fixture with middleware rewrite
- Adds /nextjs-compat/action-forward-loop-rewrite as rewrite target
- Adds error boundary component using unstable_isUnrecognizedActionError
- Adds E2E tests: middleware rewrite + action POST, not-found header contract

* test(e2e): fix action-forward-loop tests per review feedback

- page.tsx: use useActionState so action result renders into #action-result
- actions.ts: update runAction signature for useActionState compat
- Remove dead error-boundary.tsx (never exercised in single-worker model)
- Rename second test to match actual behavior (stale action ID -> 404)

* test(request-pipeline): add x-action-forwarded to INTERNAL_HEADERS snapshot
2026-05-18 18:07:05 +01:00
Nathan Nguyen 04368ebbcb fix(middleware): preserve credentials for external override rewrites (#1121)
Middleware request-header overrides could rebuild forwarded external rewrite requests from only the override set. That drops cookie and authorization when middleware reports a partial override, so proxied upstream requests lose credentials.

Keep Next.js-compatible deletion semantics by default, and make credential preservation an explicit opt-in for external proxy boundaries. Cover both partial preservation and explicit credential deletion.
2026-05-07 11:24:04 +01:00
Nathan Nguyen d6c270f6b8 fix: filter internal Next.js headers from inbound requests (#1056)
* fix: filter internal Next.js headers from inbound requests

Next.js strips internal headers (x-nextjs-data, x-matched-path,
x-now-route-matches, x-next-resume-state-length, and x-middleware-*)
via filterInternalHeaders() at the router-server entry point before
any handler or middleware sees the request. This prevents attackers
from forging these headers to influence routing or impersonate
internal data fetches.

vinext was not filtering any of these headers from inbound requests,
so all four reached middleware as user-controlled request headers.

Ported from Next.js INTERNAL_HEADERS list and filterInternalHeaders():
- https://github.com/vercel/next.js/blob/canary/packages/next/src/server/lib/server-ipc/utils.ts
- https://github.com/vercel/next.js/blob/canary/packages/next/src/server/lib/router-server.ts

Added filterInternalHeaders() to server/request-pipeline.ts and wired
it into all request entry points:
- App Router handler (app-rsc-handler.ts)
- CF Worker App Router entry (app-router-entry.ts)
- Node prod-server (nodeToWebRequest + Pages Router path)
- Dev server Pages Router middleware path (index.ts)
- Generated Pages Router worker entry (deploy.ts)

Filtering runs before middleware, config matching, and routing so no
subsystem sees forged internal headers. The x-middleware-* headers are
stripped from requests but NOT from middleware responses — the
middleware protocol (which reads x-middleware-* from Response headers,
not request headers) continues to work correctly.

* fix: use safe Request rebuild instead of mutating headers directly

Request.headers is immutable in Cloudflare Workers / Miniflare /
workerd environments. The previous in-place mutation via
headers.delete() threw 'Can't modify immutable headers', causing
every request to 500 in the cloudflare-dev E2E suite.

Changed filterInternalHeaders() to always return a new Headers
copy (never mutates the input). Call sites that use request.headers
now rebuild the Request with the filtered headers, matching the
existing pattern in applyMiddlewareRequestHeaders() at
config-matchers.ts:495-502.

* fix: clone requests when filtering headers

* fix: clone Requests safely across runtimes

* fix: avoid type assertions in Request cf access

* fix: move header filtering from handler body to entry boundary

handleAppRscRequest() is an internal handoff point, not a trust
boundary. Filtering here broke the x-vinext-mw-ctx handoff in hybrid
app+pages dev mode because cloned Request objects could drop
coordination state from the connect handler, causing middleware to
run twice.

Moved filtering to the top of createAppRscHandler(), the true App
Router request boundary, before headersContextFromRequest() and
requestContextFromRequest() capture any context. This also fixes the
security gap where forged headers were captured in the request context
before filtering ran.

* fix: preserve x-vinext-mw-ctx when filtering internal headers in app-rsc-handler

srvx's NodeRequestHeaders reads from rawHeaders for iteration but falls back
to req.headers for .get() / .has(). In the dev server we add x-vinext-mw-ctx
to req.headers after the Request is built, so it is visible to .get() but lost
when filterInternalHeaders iterates. Read it BEFORE iterating so
applyForwardedMiddlewareContext can skip middleware and avoid double execution.

Fixes CI failure on PR #1056.

* fix: copy cf in both clone paths, add cloneRequestWithHeaders tests

cloneRequestWithHeaders now copies the Workers cf property in both
the primary (try) path and the fallback path. Added 8 unit tests
locking down method, URL, redirect, signal, body, cf, and combined
metadata preservation to prevent regression to the broken
URL-constructor pattern.
2026-05-05 20:03:05 +01:00
Nathan Nguyen 9cae9cb271 refactor: extract early request pipeline helpers (#983) 2026-04-30 10:03:19 +01:00
Steve Faulkner c58daa61ba fix: centralize protocol-relative URL guard to handle percent-encoded delimiters (#888)
Six places in the request pipeline inlined a variation of:

  pathname.replaceAll("\\", "/").startsWith("//")

to reject protocol-relative paths before they reach the trailing-slash
redirect emitter (which would otherwise echo them into a Location header).
The check handles literal delimiters but not their percent-encoded forms —
`/%5Cevil.com/` survives the guard, then `normalizePathnameForRouteMatchStrict`
re-encodes the decoded backslash back to `%5C` (preserving it as part of a
single path segment), so the 308 trailing-slash redirect ends up with
`Location: /%5Cevil.com`. Browsers percent-decode Location headers and
WHATWG URL treats backslash as forward slash, so the browser resolves that
to a protocol-relative host.

Factor the leading-segment shape check into a new `isOpenRedirectShaped`
helper in `server/request-pipeline.ts` that recognises literal (`//`, `/\\`)
and percent-encoded (`%5C`, `%2F`) variants, and swap each call site over
to it. Also add a defense-in-depth check in `normalizeTrailingSlash` so a
future caller that skips the upstream guard still can't emit a bad Location.

Updated call sites:
  - server/request-pipeline.ts — guardProtocolRelativeUrl + normalizeTrailingSlash
  - server/prod-server.ts — App Router and Pages Router Node handlers
  - server/app-router-entry.ts — default Cloudflare Worker entry
  - server/app-ssr-entry.ts — SSR environment entry
  - index.ts — Pages Router Vite dev middleware
  - deploy.ts — generated Pages Router Cloudflare Worker (inlined, kept in sync)

Regression tests in tests/request-pipeline.test.ts cover the encoded
variants plus the defense-in-depth path. Updates the pinned string
assertion in tests/deploy.test.ts that was checking for the old inlined
pattern.
2026-04-24 15:33:17 -05:00
Steve Faulkner f910e74045 fix(app-router): reject cyclic Flight payloads in server actions (#798)
* fix(app-router): reject cyclic Flight payloads in server actions

* test(app-router): update generated entry snapshots
2026-04-08 22:11:15 -05:00
Michael Hart 6a68fd1c94 fix: harden origin validation and proxy request handling (#571)
* Treat `Origin: "null"` as an explicit cross-origin value

Also strip Transfer-Encoding

* Further hardening

* Add Vite HMR websocket token regression tests

Verify that websocket upgrades with an Origin header are rejected when the HMR token is missing or invalid, and accepted when the token matches the value exposed by @vite/client.

* Add clarifying comment and share a single dev server
2026-03-16 21:24:23 -05:00
Stephen Zhou c17d6941be chore: migrate to vite plus (#535)
* chore: migrate to vite plus

* Disable typeAware and typeCheck

* Update CI

* Fix CI

* Fix test

* Clean

* Run test with vp

* Try revert

* react: false In test

* Fix test

* Revert "Try revert"

This reverts commit 009da10473.

* Update

* Update

* Try revert ci changes

* revert

* Run vp migrate

* Disable typeAware and typeCheck for now

* Better resolve for test

* Use vp dev instead of vite

* Update expect

* Fix NormalizeManifestModuleId

* Try increase timeout

* Update to use vp

* Try new check

* Bring back npx vp

* Migrate CI

* Make next-intl resolvable

* Update

* Update

* Update
2026-03-15 10:50:13 +00:00
Jared Stowell dea3c3e44f fix: enforce segment boundaries for basePath stripping and redirect prefixing (#393)
* Update deploy basePath stripping

* Fix redirect basePath handling

* Document basePath segment fix

* Fix external redirect basePath
2026-03-10 08:03:20 +00:00
MD YUNUS 1bba737dc4 refactor: extract shared request handling from server entry points (#258)
* refactor: extract shared request handling into config-matchers and request-pipeline

Deduplicate ~250 lines of inline request-handling logic from the generated
App Router RSC entry (app-dev-server.ts) and Pages Router server entry
(index.ts) by importing from the canonical config-matchers.ts and a new
request-pipeline.ts module.

Changes:
- Create server/request-pipeline.ts with shared utilities:
  guardProtocolRelativeUrl, stripBasePath, normalizeTrailingSlash,
  validateCsrfOrigin, isOriginAllowed, validateImageUrl,
  processMiddlewareHeaders
- app-dev-server.ts: replace 14 inline __-prefixed functions with imports
  from config-matchers.ts (matchRedirect, matchRewrite, matchHeaders,
  isExternalUrl, proxyExternalRequest, sanitizeDestination,
  requestContextFromRequest) and request-pipeline.ts (validateCsrfOrigin,
  validateImageUrl)
- index.ts: replace inline parseCookies, sanitizeDestinationLocal, and
  matchConfigPattern with imports/re-exports from config-matchers.ts;
  replace manual RequestContext construction with requestContextFromRequest()
- Update tests to reference imported function names instead of old inline
  __-prefixed names

Security fixes:
- matchConfigPattern: eliminates chained .replace() divergence (CodeQL
  flagged) in favor of config-matchers single-pass tokenizer
- proxyExternalRequest: uses AbortController+setTimeout (broader runtime
  support) instead of AbortSignal.timeout()
- escapeHeaderSource: uses proper config-matchers implementation instead of
  simplified chained .replace()

Closes #254

t status

* fix: address bonk review - remove dead applyTo param, dangling comment

* fix: remove dangling comments (parseCookies stub, sanitizeDestination stub)

* fix: remove bare block around __tsRedirect, add multiple trailing slash test

* fix: stricter /api prefix check for trailing slash normalization (matches only /api or /api/), add tests for edge cases

* test: update entry-template snapshots after merging main

---------

Co-authored-by: James <james@eli.cx>
2026-03-09 10:28:10 +00:00