Pictures: at most 16 megapixels and 64 MB decoded per upload (security M-3)

AC-4.4 allowed 8000 x 8000 pixels: a few kilobytes of flat PNG or WebP decode to 256 MB RGBA, more than the server
container's 160 MiB. Now (proposed D-114) the header must also give at most 16 000 000 pixels and at most 64 MB
decoded (16 MP x 4 bytes; a 16-bit image is refused above 8 MP), checked before decoding, with the image crate's
decoder limits set as well; refused with the existing image_invalid. The decoded image is dropped before the square
is converted, and the square converts in place (into_rgba8/into_rgb8), so the peak is one decoded image plus one
square. Integration test at and over the pixel cap; unit test of the size rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
2026-10-18 22:42:35 +02:00
parent 725901c98f
commit baa5806812
2 changed files with 97 additions and 17 deletions
@@ -230,6 +230,25 @@ async fn e9_unsupported_media_type_image_invalid_and_payload_too_large() {
assert_eq!(me.json["user"]["picture_version"], 1);
}
#[tokio::test]
async fn e9_refuses_more_than_16_megapixels() {
// D-114: a few kilobytes of flat PNG may decode to a huge image; the pixel count is capped before decoding.
let app = TestApp::household().await;
let over = encode(
DynamicImage::ImageLuma8(image::GrayImage::new(8000, 2001)),
ImageFormat::Png,
);
assert!(over.len() < 5 * 1024 * 1024);
let reply = upload(&app, &admin_token(), Some("image/png"), over).await;
assert_error(&reply, 422, "image_invalid");
let at_cap = encode(
DynamicImage::ImageLuma8(image::GrayImage::new(8000, 2000)),
ImageFormat::Png,
);
let reply = upload(&app, &admin_token(), Some("image/png"), at_cap).await;
assert_eq!(reply.status, 200, "{}", reply.text());
}
#[tokio::test]
async fn e10_removes_idempotently_and_the_next_upload_continues_the_count() {
let app = TestApp::household().await;