From afb2e4f7284dcbc2f05ce14c76ba4f3f909c5148 Mon Sep 17 00:00:00 2001 From: Zhengchao An Date: Mon, 24 Aug 2026 21:03:13 +0800 Subject: [PATCH] fix(multipart): enforce complete part number limit (#6528) --- crates/e2e_test/src/security_boundary_test.rs | 36 +++++++++++++++---- rustfs/src/app/multipart_usecase.rs | 34 +++++++++++++++++- rustfs/src/app/storage_api.rs | 1 + 3 files changed, 63 insertions(+), 8 deletions(-) diff --git a/crates/e2e_test/src/security_boundary_test.rs b/crates/e2e_test/src/security_boundary_test.rs index 8c5c7d469..34d026c12 100644 --- a/crates/e2e_test/src/security_boundary_test.rs +++ b/crates/e2e_test/src/security_boundary_test.rs @@ -87,7 +87,7 @@ async fn test_large_xml_body_rejection() -> Result<(), Box Result<(), Box> { init_logging(); @@ -107,11 +107,24 @@ async fn test_excessive_multipart_parts() -> Result<(), Box Result<(), Box S3Result<()> { + if let Some(part) = parts + .iter() + .find(|part| !(1..=MAX_MULTIPART_PART_NUMBER as usize).contains(&part.part_num)) + { + return Err(S3Error::with_message( + S3ErrorCode::InvalidPart, + format!("Part number {} must be between 1 and {MAX_MULTIPART_PART_NUMBER}", part.part_num), + )); + } + if parts.windows(2).any(|window| window[0].part_num >= window[1].part_num) { return Err(s3_error!(InvalidPartOrder, "Part numbers must be strictly increasing")); } @@ -2504,6 +2516,26 @@ mod tests { assert_eq!(err.code(), &S3ErrorCode::InvalidPartOrder); } + #[test] + fn validate_complete_multipart_parts_enforces_part_number_range() { + validate_complete_multipart_parts(&[CompletePart { + part_num: MAX_MULTIPART_PART_NUMBER as usize, + ..Default::default() + }]) + .expect("part number 10000 must remain valid"); + + for part_num in [0, MAX_MULTIPART_PART_NUMBER as usize + 1] { + let err = validate_complete_multipart_parts(&[CompletePart { + part_num, + ..Default::default() + }]) + .expect_err("out-of-range complete part number must be rejected"); + assert_eq!(err.code(), &S3ErrorCode::InvalidPart); + let expected = format!("Part number {part_num} must be between 1 and {MAX_MULTIPART_PART_NUMBER}"); + assert_eq!(err.message(), Some(expected.as_str())); + } + } + #[test] fn normalize_complete_multipart_parts_keeps_last_duplicate_part() { let input = vec![ diff --git a/rustfs/src/app/storage_api.rs b/rustfs/src/app/storage_api.rs index c31c18f36..3c085ad9b 100644 --- a/rustfs/src/app/storage_api.rs +++ b/rustfs/src/app/storage_api.rs @@ -1181,6 +1181,7 @@ pub(crate) mod multipart_usecase { pub(crate) mod multipart { pub(crate) use super::super::super::storage_contracts::{CompletePart, MultipartOperations, MultipartUploadResult}; + pub(crate) use crate::storage::storage_api::s3_api_consumer::multipart::contract::multipart::MAX_MULTIPART_PART_NUMBER; } pub(crate) mod object {