fix: cap the multipart metadata part instead of buffering it unbounded
The `metadata` JSON part in the manga-create and chapter-upload handlers was read with `Field::bytes()`, buffering the whole part before any check — a client could send a multi-hundred-MB metadata blob (up to the 200 MiB request limit) as one allocation. Image parts were already streamed with a cap; metadata was the last unbounded read. Cap it at a shared upload::MAX_METADATA_BYTES (64 KiB) via the read_capped streaming loop (413 on overflow), and remove the now-unused read_field_bytes helper so the unbounded path can't return. Tests: manga + chapter oversized-metadata parts are rejected with 413. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@ use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::api::mangas::{next_field, read_field_bytes};
|
||||
use crate::api::mangas::next_field;
|
||||
use crate::api::pagination::PagedResponse;
|
||||
use crate::app::AppState;
|
||||
use crate::auth::extractor::CurrentUser;
|
||||
@@ -107,7 +107,12 @@ async fn create(
|
||||
while let Some(field) = next_field(&mut multipart).await? {
|
||||
match field.name() {
|
||||
Some("metadata") => {
|
||||
let bytes = read_field_bytes(field).await?;
|
||||
let bytes = crate::upload::read_capped(
|
||||
field,
|
||||
crate::upload::MAX_METADATA_BYTES,
|
||||
"metadata",
|
||||
)
|
||||
.await?;
|
||||
metadata = Some(serde_json::from_slice(&bytes).map_err(|e| {
|
||||
AppError::ValidationFailed {
|
||||
message: "metadata is not valid JSON".into(),
|
||||
|
||||
@@ -235,7 +235,12 @@ async fn create(
|
||||
while let Some(field) = next_field(&mut multipart).await? {
|
||||
match field.name() {
|
||||
Some("metadata") => {
|
||||
let bytes = read_field_bytes(field).await?;
|
||||
let bytes = crate::upload::read_capped(
|
||||
field,
|
||||
crate::upload::MAX_METADATA_BYTES,
|
||||
"metadata",
|
||||
)
|
||||
.await?;
|
||||
metadata = Some(parse_metadata_json(&bytes)?);
|
||||
}
|
||||
Some("cover") => {
|
||||
@@ -677,12 +682,6 @@ pub(crate) async fn next_field(
|
||||
.map_err(map_multipart_error)
|
||||
}
|
||||
|
||||
pub(crate) async fn read_field_bytes(
|
||||
field: axum::extract::multipart::Field<'_>,
|
||||
) -> AppResult<axum::body::Bytes> {
|
||||
field.bytes().await.map_err(map_multipart_error)
|
||||
}
|
||||
|
||||
pub(crate) fn map_multipart_error(e: axum::extract::multipart::MultipartError) -> AppError {
|
||||
let status = e.status();
|
||||
if status == StatusCode::PAYLOAD_TOO_LARGE {
|
||||
|
||||
Reference in New Issue
Block a user