From 3a6f630ff1ee316eb0c7fec1a411a0276dc903d7 Mon Sep 17 00:00:00 2001 From: Zhengchao An Date: Tue, 4 Aug 2026 23:13:25 +0800 Subject: [PATCH] fix(api): bound bucket rate limiter keys (#5706) --- rustfs/src/server/rate_limit.rs | 34 +++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/rustfs/src/server/rate_limit.rs b/rustfs/src/server/rate_limit.rs index 16d8b92e6..864658fdb 100644 --- a/rustfs/src/server/rate_limit.rs +++ b/rustfs/src/server/rate_limit.rs @@ -84,6 +84,7 @@ pub(crate) const RATE_LIMIT_SCOPE_CONSOLE: &str = "console"; pub(crate) const LABEL_RATE_LIMIT_DIMENSION: &str = "dimension"; pub(crate) const RATE_LIMIT_DIMENSION_CLIENT_IP: &str = "client_ip"; const RATE_LIMIT_DIMENSION_BUCKET: &str = "bucket"; +const MAX_BUCKET_NAME_LENGTH: usize = 63; // Header names shared with the Swift error mapping (crates/protocols swift/errors.rs). const X_RATE_LIMIT_LIMIT: &str = "x-ratelimit-limit"; @@ -332,6 +333,10 @@ fn strip_vh_prefix<'a>(host: &'a str, domain: &str) -> Option<&'a str> { } } +fn bounded_bucket_name(bucket: &str) -> Option<&str> { + (!bucket.is_empty() && bucket.len() <= MAX_BUCKET_NAME_LENGTH).then_some(bucket) +} + /// Extract the bucket a request addresses, if any. /// /// Mirrors s3s host routing: on a configured virtual-hosted-style domain the @@ -355,13 +360,13 @@ fn request_bucket<'a, B>(req: &'a Request, vh_domains: &[String]) -> Option<& break; } if let Some(bucket) = strip_vh_prefix(host, domain) { - return Some(bucket); + return bounded_bucket_name(bucket); } } } let bucket = path.trim_start_matches('/').split('/').next().unwrap_or(""); - (!bucket.is_empty()).then_some(bucket) + bounded_bucket_name(bucket) } /// Paths exempt from S3 API rate limiting. @@ -796,6 +801,19 @@ mod tests { assert_eq!(request_bucket(&root, &domains), None); } + #[test] + fn request_bucket_rejects_oversized_path_segment() { + let domains: Vec = vec![]; + let maximum = "a".repeat(MAX_BUCKET_NAME_LENGTH); + let maximum_path = format!("/{maximum}/object"); + let maximum_req = request_with_host("s3.example.com", &maximum_path); + assert_eq!(request_bucket(&maximum_req, &domains), Some(maximum.as_str())); + + let oversized_path = format!("/{}/object", "a".repeat(MAX_BUCKET_NAME_LENGTH + 1)); + let oversized_req = request_with_host("s3.example.com", &oversized_path); + assert_eq!(request_bucket(&oversized_req, &domains), None); + } + #[test] fn request_bucket_resolves_virtual_hosted_style_against_domains() { let domains = vec!["s3.example.com".to_string(), "s3.example.com:9000".to_string()]; @@ -818,6 +836,18 @@ mod tests { assert_eq!(request_bucket(&other, &domains), Some("photos")); } + #[test] + fn request_bucket_rejects_oversized_virtual_host_prefix() { + let domains = vec!["s3.example.com".to_string()]; + let maximum = "a".repeat(MAX_BUCKET_NAME_LENGTH); + let maximum_req = request_with_host(&format!("{maximum}.s3.example.com"), "/object"); + assert_eq!(request_bucket(&maximum_req, &domains), Some(maximum.as_str())); + + let oversized = "a".repeat(MAX_BUCKET_NAME_LENGTH + 1); + let oversized_req = request_with_host(&format!("{oversized}.s3.example.com"), "/object"); + assert_eq!(request_bucket(&oversized_req, &domains), None); + } + #[test] fn request_bucket_skips_admin_and_catalog_namespaces() { let domains: Vec = vec![];