diff --git a/rustfs/src/app/multipart_usecase.rs b/rustfs/src/app/multipart_usecase.rs index f538d579f..b68b93717 100644 --- a/rustfs/src/app/multipart_usecase.rs +++ b/rustfs/src/app/multipart_usecase.rs @@ -26,7 +26,7 @@ use crate::storage::options::{ }; use crate::storage::s3_api::multipart::{ ListMultipartUploadsParams, build_list_multipart_uploads_output, build_list_parts_output, - parse_list_multipart_uploads_params, parse_list_parts_params, + parse_list_multipart_uploads_params, parse_list_parts_params, parse_upload_part_number, }; use crate::storage::sse::{build_ssec_read_headers, encryption_material_to_metadata, map_get_object_reader_error}; use crate::storage::*; @@ -668,7 +668,7 @@ impl DefaultMultipartUsecase { .. } = input; - let part_id = part_number as usize; + let part_id = parse_upload_part_number(part_number)?; let mut size = content_length; let mut body_stream = body.ok_or_else(|| s3_error!(IncompleteBody))?; @@ -1006,7 +1006,7 @@ impl DefaultMultipartUsecase { None }; - let part_id = part_number as usize; + let part_id = parse_upload_part_number(part_number)?; let Some(store) = new_object_layer_fn() else { return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string())); @@ -1726,6 +1726,29 @@ mod tests { assert_eq!(err.code(), &S3ErrorCode::InternalError); } + #[tokio::test] + async fn execute_upload_part_copy_rejects_invalid_part_number_before_store_lookup() { + for part_number in [-1, 0, 10001] { + let input = UploadPartCopyInput::builder() + .bucket("bucket".to_string()) + .key("object".to_string()) + .copy_source(CopySource::Bucket { + bucket: "src-bucket".into(), + key: "src-object".into(), + version_id: None, + }) + .part_number(part_number) + .upload_id("upload-id".to_string()) + .build() + .unwrap(); + let req = build_request(input, Method::PUT); + + let err = Box::pin(make_usecase().execute_upload_part_copy(req)).await.unwrap_err(); + assert_eq!(err.code(), &S3ErrorCode::InvalidArgument); + assert_eq!(err.message(), Some("partNumber must be between 1 and 10000")); + } + } + #[test] fn test_validate_copy_source_range_not_exceeds_returns_invalid_range_when_range_exceeds() { use super::validate_copy_source_range_not_exceeds; @@ -1778,4 +1801,22 @@ mod tests { let err = make_usecase().execute_upload_part(req).await.unwrap_err(); assert_eq!(err.code(), &S3ErrorCode::IncompleteBody); } + + #[tokio::test] + async fn execute_upload_part_rejects_invalid_part_number_before_body_lookup() { + for part_number in [-1, 0, 10001] { + let input = UploadPartInput::builder() + .bucket("bucket".to_string()) + .key("object".to_string()) + .upload_id("upload-id".to_string()) + .part_number(part_number) + .build() + .unwrap(); + let req = build_request(input, Method::PUT); + + let err = make_usecase().execute_upload_part(req).await.unwrap_err(); + assert_eq!(err.code(), &S3ErrorCode::InvalidArgument); + assert_eq!(err.message(), Some("partNumber must be between 1 and 10000")); + } + } } diff --git a/rustfs/src/storage/s3_api/multipart.rs b/rustfs/src/storage/s3_api/multipart.rs index ea7ea3f06..95628d76d 100644 --- a/rustfs/src/storage/s3_api/multipart.rs +++ b/rustfs/src/storage/s3_api/multipart.rs @@ -19,6 +19,7 @@ use s3s::dto::{CommonPrefix, ListMultipartUploadsOutput, ListPartsOutput, Multip use s3s::{S3Error, S3ErrorCode}; const MAX_MULTIPART_UPLOADS_LIST: i32 = 1000; +const MAX_MULTIPART_PART_NUMBER: i32 = 10000; #[derive(Debug, PartialEq, Eq)] pub(crate) struct ListPartsParams { @@ -103,6 +104,17 @@ pub(crate) fn parse_list_parts_params( }) } +pub(crate) fn parse_upload_part_number(part_number: i32) -> Result { + if !(1..=MAX_MULTIPART_PART_NUMBER).contains(&part_number) { + return Err(S3Error::with_message( + S3ErrorCode::InvalidArgument, + format!("partNumber must be between 1 and {MAX_MULTIPART_PART_NUMBER}"), + )); + } + + Ok(part_number as usize) +} + pub(crate) fn parse_list_multipart_uploads_params( prefix: Option, key_marker: Option, @@ -176,7 +188,7 @@ pub(crate) fn build_list_multipart_uploads_output( mod tests { use super::{ MAX_MULTIPART_UPLOADS_LIST, build_list_multipart_uploads_output, build_list_parts_output, - parse_list_multipart_uploads_params, parse_list_parts_params, + parse_list_multipart_uploads_params, parse_list_parts_params, parse_upload_part_number, }; use crate::storage::s3_api::common::{rustfs_initiator, rustfs_owner}; use rustfs_ecstore::client::object_api_utils::to_s3s_etag; @@ -319,6 +331,21 @@ mod tests { assert_eq!(err.message(), Some("part-number-marker must be non-negative")); } + #[test] + fn test_parse_upload_part_number_accepts_s3_range() { + assert_eq!(parse_upload_part_number(1).expect("part 1 should be valid"), 1); + assert_eq!(parse_upload_part_number(10000).expect("part 10000 should be valid"), 10000); + } + + #[test] + fn test_parse_upload_part_number_rejects_out_of_s3_range() { + for part_number in [-1, 0, 10001] { + let err = parse_upload_part_number(part_number).expect_err("expected invalid part number"); + assert_eq!(*err.code(), S3ErrorCode::InvalidArgument); + assert_eq!(err.message(), Some("partNumber must be between 1 and 10000")); + } + } + #[test] fn test_parse_list_multipart_uploads_params_defaults_and_valid_values() { let parsed =