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:
MechaCat02
2026-07-13 20:10:05 +02:00
parent 253d46c7e5
commit 1b7b8a3038
8 changed files with 71 additions and 12 deletions

View File

@@ -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(),

View File

@@ -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 {