fix(config): preserve destination path boundaries - #2589
Conversation
|
/bigbonk review for issues |
commit: |
|
Performance benchmarksCompared 0 improved · 0 regressed · 6 within ±1.5%
View detailed results and traces 🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head |
There was a problem hiding this comment.
Review: preserve destination path boundaries
Reviewed the config-matcher substitution hardening, the middleware/URL construction changes, and dev/Node-prod parity. The security logic in config-matchers.ts is solid — condition captures are consistently encoded per URL component, source-capture dot segments are re-encoded before WHATWG parsing can collapse them, and isValidSubstitutedExternalDestination fails closed on scheme/authority injection. Test coverage of the matcher is thorough.
I found one significant gap.
Cloudflare Workers Pages Router path is not covered (dev/prod/worker parity)
The fix threads a separately-computed normalizedPathname into runPagesRequest so a WHATWG new URL() parse can't collapse encoded dot segments before config redirect/rewrite matching. This is wired up in:
- dev —
index.ts(middlewareUrl+req.url) - Node prod —
server/prod-server.ts:1861passesnormalizedPathname: pathname, and builds theRequestfrom the encodedrequestPathname
But the Cloudflare Workers Pages Router entry (server/pages-router-entry.ts) — the documented primary deployment target — was not updated:
pages-router-entry.ts:112-113derivespathnamevianew URL(request.url).pathname, which collapses%2e%2e/%2e./ literal...pages-router-entry.ts:204callsrunPagesRequest(request, deps)withoutnormalizedPathname, sopages-request-pipeline.ts:261(deps.normalizedPathname ?? url.pathname) re-parsesrequest.urland gets the already-collapsed path.
Concretely (new URL() behavior):
/proxy/%2e%2e/admin -> /admin
/proxy/%2e./admin -> /admin
/proxy/../admin -> /admin
/proxy/%252e%252e/... -> /proxy/%252e%252e/... (double-encoded survives)
So on Workers, config redirects/rewrites and middleware-visible URLs see the collapsed path for the single-encoded traversal cases, diverging from dev and Node prod. wrapMiddlewareWithBasePath (line 185) also rebuilds the URL via mwUrl.pathname = ..., re-encoding again.
The App Router Workers path is fine — app-router-entry.ts:149-158 deliberately hands the raw request to the RSC handler, which owns the single decode/normalize pass, so encoding is preserved there.
This matches the parity class of bug called out in AGENTS.md ("check whether the same bug exists in ... cloudflare/worker-entry.ts"). The three new E2E specs exercise Pages dev (4173) and Node prod (4175) only; there is no Workers coverage for the encoded-dot cases, so CI won't catch this.
Suggested fix: in pages-router-entry.ts, compute the once-normalized pathname (mirroring prod-server.ts) and pass it as deps.normalizedPathname, and construct the Request handed to runPagesRequest from the encoded inbound pathname (not a new URL().pathname round-trip) so middleware sees the original encoded URL, matching Node prod.
Minor / non-blocking
pages-router-entry.ts:140-142setsstrippedUrl.pathname = strippedwhen stripping basePath, which re-encodes via WHATWG URL. Even oncenormalizedPathnameis threaded through, the middleware-visiblerequest.urlon Workers can still differ from the raw inbound path for encoded-dot requests inside a basePath. Node prod avoids this by keeping a separaterequestPathnamestring. Worth aligning in the same change.- The pre-Vite dispatch in
index.ts:4431only handles mixed encoded-dot segments (%2e./.%2e), intentionally excluding fully-encoded%2e%2e(no literal.for Vite to treat as an extension). That scoping is correct; noting it so it isn't mistaken for an omission later.
Everything else — the encoding matrix, scheme-invariance guard, and the removal of normalizedPathname write-back in createNextRequest — looks correct and well-tested. The blocker is the missing Workers Pages Router parity.
|
Review posted to PR #2589. SummaryI reviewed PR #2589 ( The good: The core hardening in One significant gap (blocker): The fix threads a separate
I verified the collapse behavior empirically. The App Router Workers path is fine (it hands the raw request to the RSC handler). The three new E2E specs only cover Pages dev + Node prod, so CI won't catch the Workers divergence. This is exactly the parity class AGENTS.md warns about. Minor notes: basePath stripping in the worker re-encodes via I also confirmed the changed source files pass |
Summary
Testing
vp checkvp run vinext#build