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

@@ -358,3 +358,27 @@ async fn non_owner_can_upload_chapter(pool: PgPool) {
assert_eq!(body["title"], "Contributed");
assert_eq!(body["page_count"], 1);
}
#[sqlx::test(migrations = "./migrations")]
async fn chapter_upload_rejects_oversized_metadata_part(pool: PgPool) {
// The chapter `metadata` JSON part is capped as bytes arrive, just like the
// manga one, so a client can't buffer a huge JSON blob in memory. A ~200 KiB
// title blows the metadata cap before any page is staged.
let h = common::harness(pool);
let (_, cookie) = common::register_user(&h.app).await;
let manga_id = seed_manga(&h, &cookie, "Berserk").await;
let huge = "A".repeat(200 * 1024);
let resp = h
.app
.oneshot(common::post_multipart_with_cookie(
&format!("/api/v1/mangas/{manga_id}/chapters"),
MultipartBuilder::new()
.add_json("metadata", json!({ "number": 1, "title": huge }))
.add_file("page", "1.png", "image/png", &common::fake_png_bytes()),
&cookie,
))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
}

View File

@@ -927,3 +927,25 @@ async fn bearer_authed_admin_cannot_edit_null_uploader(pool: PgPool) {
);
}
#[sqlx::test(migrations = "./migrations")]
async fn create_rejects_oversized_metadata_part(pool: PgPool) {
// The metadata JSON part is capped well below the 200 MiB request limit so a
// client can't force the server to buffer a huge JSON blob in memory before
// any validation runs. A ~200 KiB description blows the metadata cap.
let h = common::harness(pool);
let (_, cookie) = common::register_user(&h.app).await;
let huge = "A".repeat(200 * 1024);
let resp = h
.app
.oneshot(common::post_multipart_with_cookie(
"/api/v1/mangas",
MultipartBuilder::new()
.add_json("metadata", json!({ "title": "Big", "description": huge })),
&cookie,
))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
}