fix: reject invalid multipart part numbers (#3091)

Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
安正超
2026-05-27 16:09:59 +08:00
committed by GitHub
parent ceffe21f75
commit f53eb4ad44
2 changed files with 72 additions and 4 deletions
+44 -3
View File
@@ -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"));
}
}
}
+28 -1
View File
@@ -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<usize, S3Error> {
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<String>,
key_marker: Option<String>,
@@ -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 =