From d9025f783af51233c312b8c1c18f5617d77b6493 Mon Sep 17 00:00:00 2001 From: fabi Date: Tue, 30 Jun 2026 20:26:57 +0200 Subject: [PATCH] fix(security): port image-decode DoS cap + audit backlog/media-auth docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From the 2026-06-27 audit branch (fix/audit-2026-06-27-critical-medium), whose work was ~80% absorbed into main via the batch branches. This ports the pieces that were NOT in main: - compression.rs: cap image decode with image::Limits (12000x12000, 256 MiB max_alloc) via ImageReader instead of image::open(). The upload body-size cap bounds the file on disk but not the *decoded* dimensions, so a small decompression-bomb image could OOM the box during decode/resize. Surgical port of the audit's decode guard only (not its image/video semaphore split). - docs/SECURITY-BACKLOG.md: the audit's triaged backlog, reconciled against main (each item tagged done / open / contingent-on-signed-media). Records the still- open items: host moderation UI gap, owner-delete SSE broadcast, quota mount-detection (starts_with) + low-disk guard. - docs/DECISION-media-auth.md: writes up the one real architectural divergence — main serves media unauthenticated (ServeDir + UUID-capability) while the audit built a signed/TTL'd gateway. Lays out the tradeoff (leaked URL = permanent vs ~24h access; banned-user access) and a recommendation, for a human decision. Verified: cargo build clean. Co-Authored-By: Claude Opus 4.8 --- backend/src/services/compression.rs | 17 +++++- docs/DECISION-media-auth.md | 79 ++++++++++++++++++++++++++ docs/SECURITY-BACKLOG.md | 88 +++++++++++++++++++++++++++++ 3 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 docs/DECISION-media-auth.md create mode 100644 docs/SECURITY-BACKLOG.md 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 +``/`