diff --git a/backend/src/services/compression.rs b/backend/src/services/compression.rs index d42e990..504f278 100644 --- a/backend/src/services/compression.rs +++ b/backend/src/services/compression.rs @@ -93,8 +93,21 @@ impl CompressionWorker { // Run blocking image operations in a spawn_blocking task tokio::task::spawn_blocking(move || -> Result<()> { - let img = image::open(&original) - .context("failed to open image")?; + // Reject decompression bombs *before* fully decoding: the upload body + // cap bounds the file size on disk, but a small file can still decode to + // enormous dimensions (e.g. a ~1 MB image expanding to 50k×50k px → + // gigabytes), OOM-ing the box during decode/resize. 12000×12000 covers + // any real phone photo; max_alloc hard-caps the decode allocation. + let mut reader = image::ImageReader::open(&original) + .context("failed to open image")? + .with_guessed_format() + .context("failed to read image header")?; + let mut limits = image::Limits::default(); + limits.max_image_width = Some(12_000); + limits.max_image_height = Some(12_000); + limits.max_alloc = Some(256 * 1024 * 1024); + reader.limits(limits); + let img = reader.decode().context("failed to decode image")?; // Resize to max 800px wide, preserving aspect ratio let preview = img.resize(800, 800, image::imageops::FilterType::Lanczos3); diff --git a/docs/DECISION-media-auth.md b/docs/DECISION-media-auth.md new file mode 100644 index 0000000..6db4208 --- /dev/null +++ b/docs/DECISION-media-auth.md @@ -0,0 +1,79 @@ +# Decision: media serving — unauthenticated UUID vs. signed gateway + +**Status:** OPEN — needs a call. Written 2026-06-30 from the 2026-06-27 audit +(`fix/audit-2026-06-27-critical-medium`), which implemented the signed-gateway option that +`main` did not adopt. + +**Scope:** how `main` serves uploaded photos/videos (originals + previews/thumbnails) to the +``/`