Skip to content

fix(router): ignore a cross-origin Referer as navigation context (#1005) - #1019

Merged
ivogt merged 1 commit into
mainfrom
fix/1005-cross-origin-referer
Oct 3, 2026
Merged

ivogt merged 1 commit into
mainfrom
fix/1005-cross-origin-referer

Conversation

@ivogt

@ivogt ivogt commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #1005.

Problem

A partial request with no X-RSC-Router-Client-Path used the Referer header as the navigation source, then matched only that URL's pathname. An absolute cross-origin Referer kept its foreign origin, but the pathname was looked up in this app. https://other.example/product/42 was matched as /product/42. That source drives the segment diff, intercept selection, and intercept when's from.

The router's own client always sends X-RSC-Router-Client-Path. This only affects a partial request made without it. A same-origin Referer is still a useful fallback for those requests.

Before / after

// Partial GET /product/99 with no X-RSC-Router-Client-Path.
// Referer: https://other.example/product/42

Before, the router treated the visitor as coming from this app's /product/42: that route's params and name were the navigation source. After, that request has no navigation context, the same as a missing Referer. A Referer of https://shop.example/product/42 on a request whose match origin is https://shop.example is still the source.

No call-site change. Apps do not adopt a new API.

End-to-end consumer example

A product page is also an intercept target. A monitoring client, or any partial fetch that omits X-RSC-Router-Client-Path, requests /product/99 with Referer: https://news.example/product/42.

Before, intercept when's from and the segment diff were computed as if the visitor had been on this shop's /product/42. After, the request is not a navigation from that page. The same fetch with Referer: https://shop.example/cart still navigates from /cart.

Semantics

Partial request Before After
X-RSC-Router-Client-Path set that value is the source, Referer ignored unchanged
No client-path header, no Referer no navigation context unchanged
Same-origin Referer, including a relative Referer that stays on the match origin that URL is the source unchanged
Referer with a different scheme, host, or port pathname matched as a route on this app no navigation context
Referer that does not parse no navigation context (resolveNavigation already rejected it) no navigation context, decided in the shared helper before the match
PPR replay gate for the same request used the same header, so a cross-origin Referer was navigation context same decision as resolveNavigation, before any shell-store read
Origin guard (Origin / Referer vs Host) unchanged unchanged. That check is not the navigation source

The origin compared is url.origin, the origin pathname matching already uses. It is not request.url's origin when those differ.

Code-change summary

getNavigationContextHeader in packages/rangojs-router/src/router/navigation-snapshot.ts now takes the match origin. It returns X-RSC-Router-Client-Path unchanged. A Referer is returned only when new URL(referer, requestOrigin) stays on that origin. Both callers pass url.origin: resolveNavigation, and matchPartialWithPprReplay in packages/rangojs-router/src/rsc/rsc-rendering.ts. The helper is the shared predicate those two already had to keep in lockstep. src/rsc/origin-guard.ts is not involved.

Test plan

  • packages/rangojs-router/src/router/__tests__/navigation-snapshot.test.ts — 31 passed. New cases, red before the fix where the old fallback returned the foreign URL:
    • treats a cross-origin Referer as no navigation context (red: expected https://other.example/product/42 to be null)
    • prefers X-RSC-Router-Client-Path over a cross-origin Referer
    • keeps a relative Referer that resolves on the match origin
    • drops a Referer whose port differs from the match origin
    • drops a Referer whose scheme differs from the match origin
    • drops a Referer that cannot be parsed
    • compares Referer to the origin argument, not request.url
  • The existing same-origin Referer fallback stayed green.
  • pnpm run typecheck
  • pnpm run test:unit:all
  • pnpm run lint
  • pnpm run format
  • Dev and production e2e. Not added. The case is a header predicate on resolveNavigation and the replay gate. The gate was not executed on its own; it calls this helper with url.origin.

Notes

Low priority for apps on the router's client, which always sends X-RSC-Router-Client-Path. The fallback remains for a same-origin Referer.

Docs and skills (added 2026-10-03)

Rebased onto main (0.20.0 plus #1016) and extended with documentation:

  • skills/ppr/SKILL.md: the no-navigation-context bullet and token-table row say "no X-RSC-Router-Client-Path and no same-origin Referer".
  • skills/intercept/SKILL.md: where from comes from, and that a request without usable navigation context gets the full page.
  • docs/internal/execution-model.md: the same predicate, named.
  • docs/internal/security-checklist.md: new section "Navigation Context from Referer", including that X-Forwarded-Host / X-Forwarded-Proto are not consulted (as with originCheck), so a header-less partial request behind a proxy that hands the app an internal URL takes the full match.
  • CHANGELOG.md: an ## Unreleased entry with the before/after table.

Gate on the rebased commit: pnpm run typecheck, pnpm run test:unit:all (router 7100, RSC 440), pnpm run lint, pnpm run format, check:docs-api, check:e2e-bucketing, skills and doc-link guards.

@pkg-pr-new

pkg-pr-new Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@rangojs/router@1019

commit: 548d41a

A partial request without X-RSC-Router-Client-Path fell back to
Referer and matched only its pathname, so a foreign URL was treated
as a route on this app. A Referer counts only when it stays on the
origin pathname matching uses. The same-origin fallback stays.

Docs: the ppr skill (no-navigation-context bullet and token table), the
intercept skill (where `from` comes from), execution-model.md, a
"Navigation Context from Referer" section in security-checklist.md, and
an Unreleased CHANGELOG entry with the before/after table and the
forwarded-header caveat.

Closes #1005.
@ivogt
ivogt force-pushed the fix/1005-cross-origin-referer branch from fd065b7 to 548d41a Compare October 3, 2026 12:10
@ivogt
ivogt merged commit d42b56e into main Oct 3, 2026
43 checks passed
@ivogt
ivogt deleted the fix/1005-cross-origin-referer branch October 3, 2026 12:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Partial requests without X-RSC-Router-Client-Path match a cross-origin Referer's pathname as the navigation source

1 participant