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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user