fix(multipart): count ignored fields towards `MultipartFormConfig` li… (#4026)

fix(multipart): count ignored fields towards `MultipartFormConfig` limits
This commit is contained in:
Yuki Okushi 2026-04-18 00:57:50 +09:00 committed by GitHub
parent be62050f9d
commit 4434a494ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 44 additions and 4 deletions

View File

@ -227,7 +227,7 @@ pub fn impl_multipart_form(input: proc_macro::TokenStream) -> proc_macro::TokenS
::actix_multipart::MultipartError::UnknownField(field.name().unwrap().to_string()) ::actix_multipart::MultipartError::UnknownField(field.name().unwrap().to_string())
)) ))
} else { } else {
quote!(::std::result::Result::Ok(())) quote!(::actix_multipart::form::discard_field(field, limits).await)
}; };
// Value for duplicate action // Value for duplicate action
@ -289,7 +289,7 @@ pub fn impl_multipart_form(input: proc_macro::TokenStream) -> proc_macro::TokenS
) -> ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = ::std::result::Result<(), ::actix_multipart::MultipartError>> + 't>> { ) -> ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = ::std::result::Result<(), ::actix_multipart::MultipartError>> + 't>> {
match field.name().unwrap() { match field.name().unwrap() {
#handle_field_impl #handle_field_impl
_ => return ::std::boxed::Box::pin(::std::future::ready(#unknown_field_result)), _ => return ::std::boxed::Box::pin(async move { #unknown_field_result }),
} }
} }

View File

@ -82,7 +82,9 @@ where
) -> Self::Future { ) -> Self::Future {
if state.contains_key(&field.form_field_name) { if state.contains_key(&field.form_field_name) {
match duplicate_field { match duplicate_field {
DuplicateField::Ignore => return Box::pin(ready(Ok(()))), DuplicateField::Ignore => {
return Box::pin(async move { discard_field(field, limits).await });
}
DuplicateField::Deny => { DuplicateField::Deny => {
return Box::pin(ready(Err(MultipartError::DuplicateField( return Box::pin(ready(Err(MultipartError::DuplicateField(
@ -159,7 +161,9 @@ where
) -> Self::Future { ) -> Self::Future {
if state.contains_key(&field.form_field_name) { if state.contains_key(&field.form_field_name) {
match duplicate_field { match duplicate_field {
DuplicateField::Ignore => return Box::pin(ready(Ok(()))), DuplicateField::Ignore => {
return Box::pin(async move { discard_field(field, limits).await });
}
DuplicateField::Deny => { DuplicateField::Deny => {
return Box::pin(ready(Err(MultipartError::DuplicateField( return Box::pin(ready(Err(MultipartError::DuplicateField(
@ -312,6 +316,16 @@ impl Limits {
} }
} }
/// Drain a field that will not be retained while still accounting for form limits.
#[doc(hidden)]
pub async fn discard_field(mut field: Field, limits: &mut Limits) -> Result<(), MultipartError> {
while let Some(chunk) = field.try_next().await? {
limits.try_consume_limits(chunk.len(), false)?;
}
Ok(())
}
/// Typed `multipart/form-data` extractor. /// Typed `multipart/form-data` extractor.
/// ///
/// To extract typed data from a multipart stream, the inner type `T` must implement the /// To extract typed data from a multipart stream, the inner type `T` must implement the
@ -710,6 +724,32 @@ mod tests {
assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.status(), StatusCode::OK);
} }
#[actix_rt::test]
async fn test_discarded_fields_count_towards_total_limit() {
let srv = actix_test::start(|| {
App::new()
.route("/unknown", web::post().to(test_upload_limits_memory))
.route("/duplicate", web::post().to(test_duplicate_ignore_route))
.app_data(
MultipartFormConfig::default()
.memory_limit(usize::MAX)
.total_limit(20),
)
});
let mut form = multipart::Form::default();
form.add_text("field", "7 bytes");
form.add_text("unknown", "this string is 28 bytes long");
let response = send_form(&srv, form, "/unknown").await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
let mut form = multipart::Form::default();
form.add_text("field", "first_value");
form.add_text("field", "this string is 28 bytes long");
let response = send_form(&srv, form, "/duplicate").await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
/// Test the Limits. /// Test the Limits.
#[derive(MultipartForm)] #[derive(MultipartForm)]
struct TestMemoryUploadLimits { struct TestMemoryUploadLimits {