mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-20 19:42:17 +00:00
fix: reject invalid multipart part numbers (#3091)
Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
@@ -26,7 +26,7 @@ use crate::storage::options::{
|
|||||||
};
|
};
|
||||||
use crate::storage::s3_api::multipart::{
|
use crate::storage::s3_api::multipart::{
|
||||||
ListMultipartUploadsParams, build_list_multipart_uploads_output, build_list_parts_output,
|
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::sse::{build_ssec_read_headers, encryption_material_to_metadata, map_get_object_reader_error};
|
||||||
use crate::storage::*;
|
use crate::storage::*;
|
||||||
@@ -668,7 +668,7 @@ impl DefaultMultipartUsecase {
|
|||||||
..
|
..
|
||||||
} = input;
|
} = input;
|
||||||
|
|
||||||
let part_id = part_number as usize;
|
let part_id = parse_upload_part_number(part_number)?;
|
||||||
|
|
||||||
let mut size = content_length;
|
let mut size = content_length;
|
||||||
let mut body_stream = body.ok_or_else(|| s3_error!(IncompleteBody))?;
|
let mut body_stream = body.ok_or_else(|| s3_error!(IncompleteBody))?;
|
||||||
@@ -1006,7 +1006,7 @@ impl DefaultMultipartUsecase {
|
|||||||
None
|
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 {
|
let Some(store) = new_object_layer_fn() else {
|
||||||
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));
|
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));
|
||||||
@@ -1726,6 +1726,29 @@ mod tests {
|
|||||||
assert_eq!(err.code(), &S3ErrorCode::InternalError);
|
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]
|
#[test]
|
||||||
fn test_validate_copy_source_range_not_exceeds_returns_invalid_range_when_range_exceeds() {
|
fn test_validate_copy_source_range_not_exceeds_returns_invalid_range_when_range_exceeds() {
|
||||||
use super::validate_copy_source_range_not_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();
|
let err = make_usecase().execute_upload_part(req).await.unwrap_err();
|
||||||
assert_eq!(err.code(), &S3ErrorCode::IncompleteBody);
|
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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use s3s::dto::{CommonPrefix, ListMultipartUploadsOutput, ListPartsOutput, Multip
|
|||||||
use s3s::{S3Error, S3ErrorCode};
|
use s3s::{S3Error, S3ErrorCode};
|
||||||
|
|
||||||
const MAX_MULTIPART_UPLOADS_LIST: i32 = 1000;
|
const MAX_MULTIPART_UPLOADS_LIST: i32 = 1000;
|
||||||
|
const MAX_MULTIPART_PART_NUMBER: i32 = 10000;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub(crate) struct ListPartsParams {
|
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(
|
pub(crate) fn parse_list_multipart_uploads_params(
|
||||||
prefix: Option<String>,
|
prefix: Option<String>,
|
||||||
key_marker: Option<String>,
|
key_marker: Option<String>,
|
||||||
@@ -176,7 +188,7 @@ pub(crate) fn build_list_multipart_uploads_output(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
MAX_MULTIPART_UPLOADS_LIST, build_list_multipart_uploads_output, build_list_parts_output,
|
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 crate::storage::s3_api::common::{rustfs_initiator, rustfs_owner};
|
||||||
use rustfs_ecstore::client::object_api_utils::to_s3s_etag;
|
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"));
|
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]
|
#[test]
|
||||||
fn test_parse_list_multipart_uploads_params_defaults_and_valid_values() {
|
fn test_parse_list_multipart_uploads_params_defaults_and_valid_values() {
|
||||||
let parsed =
|
let parsed =
|
||||||
|
|||||||
Reference in New Issue
Block a user