FileSystemRouter: fix slice panic in URLPath::parse on leading '?' or empty-decoding path by robobun · Pull Request #32669 · oven-sh/bun · GitHub
Skip to content

FileSystemRouter: fix slice panic in URLPath::parse on leading '?' or empty-decoding path#32669

Open
robobun wants to merge 1 commit into
mainfrom
farm/ce644dfc/urlpath-parse-oob
Open

FileSystemRouter: fix slice panic in URLPath::parse on leading '?' or empty-decoding path#32669
robobun wants to merge 1 commit into
mainfrom
farm/ce644dfc/urlpath-parse-oob

Conversation

@robobun

@robobun robobun commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

What

URLPath::parse panics on two classes of input that FileSystemRouter.match() forwards from user code:

router.match("?");            // panic: slice index starts at 1 but ends at 0
router.match("?foo=bar");     // same
router.match("%PUBLIC_URL%"); // panic: range start index 1 out of range for slice of length 0

Since applications pass request-derived strings to router.match(), this is a remote process-crash vector.

Cause

parse() assumes the decoded pathname is non-empty and has a leading byte to strip:

let mut path = if question_mark_i < 0 {
    &decoded_pathname[1..]
} else {
    &decoded_pathname[1..question_mark_i as usize]
};
let first_segment = &decoded_pathname[1..first_segment_end.min(len)];
  • A leading ? gives question_mark_i == 0, so the path slice is [1..0].
  • %PUBLIC_URL% is consumed entirely by the fault-tolerant percent decoder (the create-react-app placeholder special case), leaving decoded_pathname empty, so [1..] on a zero-length slice panics.

The empty/"/" guard in filesystem_router.rs runs on the raw input before decoding and does not catch either shape.

Fix

In URLPath::parse:

  • Normalise an empty decoded pathname to "/" before scanning.
  • Compute the path and first-segment slices with the start index clamped to the end index (1.min(end)..end) so a bare query string yields an empty path rather than a reversed range.

Both inputs now resolve to the index route with the query string preserved.

Verification

New test in test/js/bun/util/filesystem_router.test.ts spawns a subprocess, calls match() with "?", "?foo=bar", "%PUBLIC_URL%", and "%PUBLIC_URL%?x=1", and asserts each returns the index route with the expected query. Before this change the subprocess aborts with the slice-bounds panic above; after, all four resolve cleanly.

… empty-decoding path

URLPath::parse assumed the decoded pathname was non-empty and had a
leading byte to skip. Two user-reachable inputs violate that:

  router.match("?")            -> question_mark_i=0 -> &decoded[1..0]
  router.match("%PUBLIC_URL%") -> decoded becomes "" -> &decoded[1..]

Both produce a slice-bounds panic that takes down the process. The
existing empty/'/' guard in filesystem_router.rs runs before decoding
and does not catch the leading-'?' case, so request-derived strings
passed to router.match() could trigger this.

Fix by normalising an empty decoded pathname to '/' and clamping the
start index of the path / first_segment slices to the end index so a
bare query string yields an empty path instead of a reversed range.
@robobun

robobun commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 49bc9300-dd6b-411f-9d05-6080b2310295

📥 Commits

Reviewing files that changed from the base of the PR and between 942c222 and ac8a620.

📒 Files selected for processing (2)
  • src/http_types/URLPath.rs
  • test/js/bun/util/filesystem_router.test.ts

Walkthrough

URLPath::parse gains two bounds-safety fixes: an early guard substituting b"/" when the decoded pathname is empty, and clamped index variables (path_end, first_segment_end_u) replacing direct range slicing that could produce invalid 1..0 ranges when the query starts immediately. A regression test covers these cases via FileSystemRouter.match().

Changes

URLPath::parse degenerate input fixes

Layer / File(s) Summary
URLPath::parse bounds fixes
src/http_types/URLPath.rs
Inserts an empty-pathname guard that forces decoded_pathname to b"/" and clears decoded_storage, then replaces path/first_segment slice construction with clamped path_end and first_segment_end_u values to prevent invalid index ranges when question_mark_i points to position 0 or 1.
Regression test for degenerate router inputs
test/js/bun/util/filesystem_router.test.ts
Adds an async test that spawns a subprocess matching "?", "?foo=bar", "%PUBLIC_URL%", and "%PUBLIC_URL%?x=1" through FileSystemRouter, asserting empty stderr, correct name/query JSON for each input, and exit code 0.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately summarizes the main fix: preventing URLPath::parse slice panics on leading '?' and empty-decoding paths.
Description check ✅ Passed The description covers the bug, root cause, fix, and verification, though it uses different headings than the template.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — minimal, well-scoped bounds fix for a real slice-panic crash vector, with a regression test that follows the established subprocess pattern.

Extended reasoning...

Overview

This PR fixes two slice-bounds panics in URLPath::parse (src/http_types/URLPath.rs) that FileSystemRouter.match() could trigger on degenerate inputs: a path beginning with ? (producing a reversed [1..0] range) and an input like %PUBLIC_URL% that the fault-tolerant percent decoder consumes to an empty string (producing [1..] on a zero-length slice). The fix is two small, defensive changes: normalise an empty decoded pathname to b"/" before scanning, and clamp the slice start index to 1.min(end) for both the path and first_segment slices. A new subprocess-based regression test in filesystem_router.test.ts exercises all four shapes and asserts they resolve to the index route with the correct query.

Security risks

The bug being fixed is itself the security concern — request-derived strings reaching router.match() could crash the process (DoS). The fix introduces no new attack surface: it only narrows existing slice bounds and substitutes a static b"/" for the empty case. Setting decoded_storage = None when falling back to the static literal is correct for the lifetime-laundering scheme in this file, since all resulting field slices then point into 'static memory rather than the discarded decode buffer.

Level of scrutiny

Moderate. This is request-path parsing in core Rust, but the change is purely mechanical bounds-clamping with no control-flow or semantic changes for well-formed inputs (when path_end >= 1 and first_segment_end_u >= 1, 1.min(x) == 1 and behaviour is identical to before). I traced each of the four test inputs through the new code and confirmed no remaining out-of-range slices and that pathname/path/query_string end up with the values the test asserts.

Other factors

The test mirrors the existing subprocess pattern used by several neighbouring tests in the same file (tempDir + Bun.spawn + JSON over stdout), so a regression would surface as a nonzero exit rather than killing the runner. No prior reviewer comments to address; the bug-hunting pass found nothing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant