Repository navigation
Description
Summary
A targeted review of surfaces the original per-subsystem pass missed (the new @rangojs/router/testing module, a focused security pass, cross-subsystem seams, and an adversarial self-review) ran after the stabilization PR. The HIGH finding (cross-host loader-cache leak) and the M9 completion + MiddlewareFn export were fixed in PR #571. This issue tracks the remaining lower-severity / decision findings.
Security (defense-in-depth — mostly app-responsibility, decide on guarding in-framework)
medium — Open redirect on the no-JS (PE) / full-page path: redirect target not origin-validated server-side
The client has validate-redirect-origin.ts, but the server PE/full-page redirect path does not re-validate the target.
// If an app does this with a user-controlled value:
path("/login", async (ctx) => redirect(ctx.searchParams.get("returnTo") ?? "/"))
// ?returnTo=https://evil.example becomes an open redirect on the no-JS path.This is partly app-responsibility (don't pass unvalidated user input to redirect()), but the JS and PE paths differ in protection. Decide: add a server-side same-origin check (or opt-in allowlist) for redirects, or document the contract explicitly. progressive-enhancement.ts:130-131,169-171,236-241, redirect.ts:104-110.
low — defaultOriginCheck trusts the request URL host when the Host header is absent
origin-guard.ts:100-106 — weakens the CSRF gate when no Host/Origin header is present. Tighten to fail-closed when origin cannot be established.
low — PE direct-action passes raw FormData as action args when Flight decode fails (the JS path throws)
progressive-enhancement.ts:158-167 — JS/PE divergence; the PE path should match the JS path's reject-on-decode-failure.
low — Loaded server action is invoked before verifying the requested action ID belongs to the matched route
server-action.ts:99-100, progressive-enhancement.ts:103-108,166-167 — verify ownership before invocation.
info — Action-arg decode failure surfaces raw error detail in the thrown message (JS path) — server-action.ts:87-91.
@rangojs/router/testing module refinements (the new #533 surface)
medium — dispatch() json route lacks the nested-Promise guard production enforces
dispatch.ts:165-173 does a bare JSON.stringify(result); production (response-route-handler.ts:139-156) uses a replacer that throws RESPONSE_NOT_SERIALIZABLE on a nested unresolved promise. A dispatch json test of () => ({ data: fetchSomething() }) (forgotten await) goes green while production would 500. Reuse production's replacer so the test rejects exactly as production does.
low — renderToFlightString/renderServerTree leak ctx.reverse to the GLOBAL route map
flight.ts:175-178 — no routeMap option; setRequestContextParams rebuilds ctx.reverse against getGlobalRouteMap(), so a server component that reverses is order-dependent on whatever router registered last. Add a scoped routeMap option (like renderHandler) or document.
low — renderRoute mutates the consumer's real loader/locationState/handle objects to assign synthetic ids (render-route.tsx:74-87). Clone before stamping.
info — dispatch resolves cache config with executionContext=undefined, diverging from production (dispatch.ts:339-346).
Cross-seam (low)
- Prerendered route served on a prerender hit never records its
cache({ tags })into the document tag union (cache-lookup.ts:153-217,287-309). - Document-cache tag union can miss first-write segment-DSL tags when combined with proactive/background segment caching (documented caveat; the proactive path widens the window).
Self-review (low)
loaderParentIdregex has a residual edge for a slot name matchingD<digits>.(e.g.@D3.foo) —segment-system.tsx. Pathological, but could be hardened by validating slot names at definition time.
Note
The self-review confirmed all PR #571 stabilization fixes are correct (trie backtracking, loader wrap, passthrough detect, loud cache errors, dead-export removals). These follow-ups are refinements, not regressions.
Activity
fix(router): stabilize multi-agent review findings (15 mediums + clea…
fix(router): make redirect({ external: true }) unforgeable + scheme-v…
fix(router): strip reserved redirect marker from stub-merge primitives (
fix(router): same-origin redirects by default on PE/full-page paths +…
ivogt commentedon Jun 14, 2026
Closing — the Security defense-in-depth section is resolved by #580 (merged), and the remaining non-security items are split into a fresh, scoped issue (#582).
Security section — resolved by #580
{ external: true }opt-in is an unforgeable out-of-band brand (not a wire header), andexternalishttp(s)-only so it can't become a scriptablelocation.assign.defaultOriginChecktrusts request URL host whenHostabsentcausefor server logs.FormDatawhen Flight decode failsdecodeReplylegitimately throws for a raw<form action={fn}>POST; the[formData]fallback is the React form-action convention. The JS↔PE divergence is intentional and now documented inprogressive-enhancement.ts.Remaining items → #582
The
@rangojs/router/testingrefinements, cross-seam tag-union gaps, and theloaderParentIdself-review item are tracked in #582. Note: #581 (merged) partially touched two testing items (routeNameoption,cloneHandleSeed) but did not fully satisfy the original asks (scopedrouteMap; no consumer-object mutation) and did not touch the twodispatch.tsitems — all carried forward to #582.fix(testing): dispatch json nested-Promise guard + scoped routeMap fo…
fix(testing): dispatch json nested-Promise guard + scoped routeMap fo…
fix(testing): #582 close three @rangojs/router/testing fidelity gaps …