From cee93496587d31c514aaeced3e6b8027cfdb41a0 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:21:33 +0000 Subject: [PATCH] Remove dot segments when resolving redirect Locations Found while testing crates/fetch/src/http.rs:136 (Url::join) Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/fetch/src/http.rs | 42 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/crates/fetch/src/http.rs b/crates/fetch/src/http.rs index 0b1b0ae98..d5be997ca 100644 --- a/crates/fetch/src/http.rs +++ b/crates/fetch/src/http.rs @@ -58,6 +58,37 @@ pub struct Url { pub path: String, } +/// Remove `.` and `..` segments from the path part of `path` (RFC 3986 section 5.2.4); the query is kept as is. +fn remove_dot_segments(path: &str) -> String { + let (p, query) = match path.find('?') { + Some(i) => (path.get(..i).unwrap_or_default(), path.get(i..).unwrap_or_default()), + None => (path, ""), + }; + let mut out: Vec<&str> = Vec::new(); + let mut segments = p.split('/').skip(1).peekable(); + let mut trailing_slash = false; + while let Some(seg) = segments.next() { + let last = segments.peek().is_none(); + match seg { + "." => trailing_slash = last, + ".." => { + out.pop(); + trailing_slash = last; + } + _ => { + out.push(seg); + trailing_slash = false; + } + } + } + let mut result = format!("/{}", out.join("/")); + if trailing_slash && !result.ends_with('/') { + result.push('/'); + } + result.push_str(query); + result +} + impl Url { pub fn parse(s: &str) -> Result { let bad = |why: &str| HttpError::BadUrl(why.to_string()); @@ -81,6 +112,7 @@ impl Url { None => (rest, ""), }; let path = if path.starts_with('/') { path.to_string() } else { format!("/{path}") }; + let path = remove_dot_segments(&path); if authority.contains('@') { return Err(bad("user names and passwords in URLs are not supported")); } @@ -618,6 +650,16 @@ mod tests { assert!(Url::parse("https://example.com/x?secret=abc\r\nInjected: 1").is_err()); } + #[test] + fn join_removes_dot_segments() { + let base = Url::parse("https://a.example/x/y/file").unwrap(); + for (loc, want) in + [("../z", "/x/z"), ("./z", "/x/y/z"), ("../../../z", "/z"), ("/a/./b/../c", "/a/c"), ("../z?q=1", "/x/z?q=1"), ("..", "/x/")] + { + assert_eq!(base.join(loc).unwrap().path, want, "{loc}"); + } + } + #[test] fn parses_urls() { let u = Url::parse("https://cdn.example.com/models/sam3/model.safetensors?sig=1#x").unwrap();