fix(base): collapse leading-slash run in all base-stripping sites#1471
Conversation
Both the `mount` middleware and `requestWithBaseURL` stripped the base via a bare `.slice(base.length)`, so a request like `/api//evil.com` stripped to a protocol-relative `//evil.com`. A downstream handler redirecting to that pathname produces a protocol-relative open redirect. Route both sites through a shared `stripBase` helper that enforces a segment boundary and collapses the leading-slash run to a single slash, matching the existing `withoutBase` behavior. Co-Authored-By: Claude Fable 5 <[email protected]>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughChangesBase-path rewriting now uses shared boundary-aware slash normalization in mounted H3 applications and Base Path Normalization
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
pi0x
left a comment
There was a problem hiding this comment.
APPROVE — correct, minimal, well-tested fix for the protocol-relative open-redirect vector. Verified locally on the PR head.
What I checked
- Regression tests genuinely fail without the fix: reverting only
src/leaves all 6 new assertions red (mount fetch, mounted-H3 child middleware,requestWithBaseURLcollapse + no-match), green with it. Both stripping sites covered. - Full suite green:
pnpm vitest run test/→ 1463 passed / 39 skipped,pnpm lintclean (oxlint + oxfmt + typecheck). - Edge cases (verified against a reference impl):
/api+/api→/,/api/→/,/apifoo→unchanged (segment boundary holds),/api//evil.com→/evil.com,/api///x→/x, and crucially/api/a//b→/a//b(only the leading slash run collapses — deep multi-slash paths are preserved). Root/empty base are benign. - Query/URL consistency: mount only rewrites
event.url.pathname(search untouched);requestWithBaseURLre-serializes viaurl.href, so query survives. Good.
Bundle-size bump is justified
Measured H3 minimal bundle: pre-fix 17334 / gzip 6594 → PR 17385 / gzip 6627. New gzip exceeds the old 6600 guard, so the bump to 6650 is warranted; raw limit stays 17400 (now 15 bytes of headroom — tight but passing). Nit: the PR description says pre-fix gzip was "exactly 6600" — it actually measured 6594, but the bump is still correct.
On stripBase vs the existing withoutBase (same file)
The two overlap heavily and I initially expected reuse. But I measured it: routing both sites through withoutBase is larger (17527 / gzip 6657) because it drags withoutTrailingSlash into the core bundle. So the compact purpose-built helper is the size-conscious choice here — the duplication is defensible. Two minor notes for the author's awareness (non-blocking):
stripBasedoes not normalize a trailing-slash base (stripBase('/api/foo', '/api/')returns/api/foounstripped, wherewithoutBaseyields/foo). This is unreachable via the mount middleware (its outer guard skips trailing-slash bases) and matches the pre-existing quirk in the fetch-handler path, so no regression — but worth a comment ifstripBaseis ever reused elsewhere.- In the mount middleware,
stripBase's internal boundaryifre-checks what the outerstartsWith/segment guard already proved, so it's always-true there — negligible, and keeps the helper self-contained. The per-requestreplace(/^\/+/, "")is a cached literal regex on matched routes only; no meaningful hot-path cost.
Nice defense-in-depth aligning all base-stripping sites with the withoutBase hardening. LGTM.
🤖 Generated with Claude Code
stripBase and withoutBase each re-implemented the same security-critical segment-boundary strip + leading-slash collapse. Make stripBase the single implementation and have withoutBase delegate to it (after normalizing a trailing-slash base via withoutTrailingSlash). Core call sites keep calling the lean stripBase directly so the bundle-size budget is unaffected. Co-Authored-By: Claude Opus 4.8 <[email protected]>
Both the
mountmiddleware (src/h3.ts) andrequestWithBaseURL(src/utils/request.ts) stripped the base with a bare.slice(base.length), so a request like/api//evil.comstripped to a protocol-relative//evil.com. A downstream handler that redirects to that pathname yields a protocol-relative open redirect (withBasealready guarded against this viawithoutBase).This routes both sites through a shared internal
stripBasehelper that enforces a segment boundary and collapses the leading-slash run to a single slash.requestWithBaseURLnow also skips stripping whenbasedoesn't match on a boundary, aligning it with the mount middleware.Regression tests added for the mount fetch path, the mounted-H3 middleware path, and
requestWithBaseURL. The H3 gzip bundle limit is nudged (6.6kb -> 6.65kb) to fit the fix.🤖 Generated with Claude Code
Summary by CodeRabbit