From 1ce0174b23b491f6f7b5e747e76c30fd06d8946e Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Mon, 16 Feb 2026 04:07:22 -0800 Subject: [PATCH] feat(rust-volume-server): validate malformed fid routes in native http path --- rust/volume_server/src/main.rs | 84 ++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/rust/volume_server/src/main.rs b/rust/volume_server/src/main.rs index ed95976e4..d949ec4b8 100644 --- a/rust/volume_server/src/main.rs +++ b/rust/volume_server/src/main.rs @@ -470,6 +470,24 @@ fn try_handle_native_http( return Ok(true); } + if matches!(parsed.method.as_str(), "GET" | "HEAD" | "POST" | "PUT") { + if let Some((vid, fid)) = extract_fid_route_parts(&parsed.path) { + if !is_valid_volume_id_token(&vid) || !is_valid_fid_token(&fid) { + consume_bytes(stream, parsed.header_len + parsed.content_length)?; + write_native_http_response( + stream, + "400 Bad Request", + "text/plain; charset=utf-8", + b"", + parsed.method == "HEAD", + parsed.request_id.as_deref(), + )?; + let _ = stream.shutdown(Shutdown::Both); + return Ok(true); + } + } + } + let is_admin_control = role == ListenerRole::HttpAdmin && (parsed.path == "/status" || parsed.path == "/healthz") && (parsed.method == "GET" || parsed.method == "HEAD"); @@ -602,6 +620,72 @@ fn normalize_request_path(target: &str) -> String { .unwrap_or(raw) } +fn extract_fid_route_parts(path: &str) -> Option<(String, String)> { + let trimmed = path.trim_start_matches('/'); + if trimmed.is_empty() { + return None; + } + + let segments: Vec<&str> = trimmed.split('/').collect(); + if segments.len() == 1 { + if let Some((vid, fid_token)) = segments[0].rsplit_once(',') { + return Some((vid.to_string(), strip_optional_extension(fid_token))); + } + return None; + } + + if (segments.len() == 2 || segments.len() == 3) + && !segments[0].is_empty() + && segments[0].bytes().all(|b| b.is_ascii_digit()) + { + let fid = if segments.len() == 2 { + strip_optional_extension(segments[1]) + } else { + segments[1].to_string() + }; + return Some((segments[0].to_string(), fid)); + } + + None +} + +fn strip_optional_extension(token: &str) -> String { + if let Some((base, _)) = token.rsplit_once('.') { + if !base.is_empty() { + return base.to_string(); + } + } + token.to_string() +} + +fn is_valid_volume_id_token(vid: &str) -> bool { + !vid.is_empty() && vid.parse::().is_ok() +} + +fn is_valid_fid_token(fid: &str) -> bool { + if fid.is_empty() { + return false; + } + + let (base, delta) = match fid.rsplit_once('_') { + Some((base, delta)) => (base, Some(delta)), + None => (fid, None), + }; + if base.len() <= 8 || base.len() > 24 { + return false; + } + if !base.bytes().all(|b| b.is_ascii_hexdigit()) { + return false; + } + if let Some(delta_value) = delta { + if delta_value.is_empty() || !delta_value.bytes().all(|b| b.is_ascii_digit()) { + return false; + } + } + + true +} + fn is_public_read_method(method: &str) -> bool { matches!(method, "GET" | "HEAD" | "OPTIONS") }