GHSA-g9j2-8w95-3vwv Cloudreve: Denial of Service - Image decompression / pixel bomb in thumbnail & avatar decoding crashes the server
SummaryCloudreve's built-in image processor decodes user-supplied images with Go's standard-library decoders (`image/png`, `image/jpeg`, `image/gif`) and guards **only the compressed file size** — never the *decoded* pixel dimensions. Go's decoders allocate a pixel buffer sized `bytesPerPixel × width × height` taken straight from the image header (e.g. a PNG's IHDR), with no upper bound on `width`/`height`. A tiny (tens-of-bytes) image that *declares* enormous dimensions therefore forces a multi-gigabyte-to-terabyte allocation (`make([]uint8, …)`), exhausting memory. The resulting out-of-memory condition is a **fatal Go runtime error / kernel OOM-kill that `recover()` cannot catch**, terminating the whole Cloudreve process for all users.Two reachable sinks share the same root cause:**Avatar upload** (`PUT /api/v4/user/setting/avatar`) — decodes synchronously in the request handler. Any authenticated user. **Cleanest single-request PoC.****Thumbnail generation** (built-in generator, **enabled by default**) — decodes in the thumbnail queue worker. Reachable for the user's own files *and* for files inside a share, so a planted bomb can be (re)triggered through a public share link.Post-auth, low privilege. A single 65-byte upload deterministically takes the instance offline.DetailsRoot cause — decode guarded by file size, not pixel countThe built-in generator is the **default** image thumbnailer:`Builtin.Generate` checks the on-disk/entity size, then hands the raw bytes to the stdlib decoders:There is **no `image.DecodeConfig` pre-check** and **no dimension/pixel cap** anywhere on the path. The only `Bounds()`/`MaxWidth` references in the package (`builtin.go:70,92,125`, `avatar_size_l=200`) act on the *already-decoded* image and are the *output* resize target — they execute long after the oversized input buffer has been allocated.Why the allocation is unbounded (Go stdlib `image/png`)Go's PNG reader takes the dimensions verbatim from IHDR and rejects only non-positive values — there is no maximum:On the first IDAT, `readImagePass` allocates the destination image **before** consuming the compressed pixel data, e.g. for colour-type 6 (RGBA, 8-bit):`image.NewNRGBA` → `pixelBufferLength` → `mul3NonNeg(4, w, h)` only guards against *integer overflow* (returns −1, which panics), not against huge-but-valid sizes. So any `4·w·h` that fits in an `int` and is below the runtime's `maxAlloc` (~2⁴⁸ on amd64) proceeds to `make([]uint8, 4·w·h)`. The allocation occurs even if the IDAT stream is empty/truncated, so the malicious file needs no real pixel data.`jpeg.Decode` (allocates the `YCbCr`/`RGBA` buffer from the SOF/SOS dimensions, max 65535×65535 → up to ~17 GB) and `gif.Decode` (allocates from the logical-screen / frame dimensions) are affected the same way.Why this is a fatal crash, not a handled errorFor realistic bomb sizes (GBs–hundreds of GBs, all `< maxAlloc`), `make` proceeds and the process dies by one of:**Kernel OOM-killer** sends SIGKILL when the touched pages can't be backed — not catchable by anything; orthe Go runtime **`throw("out of memory")`** — a *fatal* error, **not** a recoverable `panic`, so `gin.Recovery()` does not save it.(Only the integer-overflow branch yields a recoverable panic; a competent attacker stays in the fatal-OOM regime, as the PoC does.)Reachability of each sink**Avatar (synchronous, in the HTTP handler):**A 65-byte PoC trivially satisfies the 4 MB `ContentLength` cap.**Thumbnail (queue worker, in-process):** `manager.Thumbnail` → `SubmitAndAwaitThumbnailTask` → `generateThumb` → `pipeline.Generate` (`pkg/filemanager/manager/thumbnail.go:128-145`, `pkg/thumb/pipeline.go:86`). The pipeline tries the **enabled-by-default** built-in generator for `png/jpg/gif` (other generators return `ErrPassThrough` for those extensions). Triggered via `GET /api/v4/file/thumb?uri=…` for the user's own files, and the same generation runs for files reached through a share (`NavigatorCapabilityGenerateThumb`), so a bomb dropped into a public share can be (re)triggered by an anonymous visitor.Proof of ConceptThe bomb (65 bytes — included as `pixelbomb.png`)Generated with:Hex (entire file):(Dimensions are tunable: `4·W·H` must stay `< 2⁶³` to avoid the overflow→panic branch and `< maxAlloc`. `2147483647 × 16` → 128 GiB reliably OOM-kills any host; shrink `H` for smaller targets.)Trigger (avatar — single request)Observable resultImpact**Direct primitive**: full availability loss — the process aborts, dropping all in-flight requests/sessions for every tenant on the instance.**Persistence / repeatability**: the bomb can be stored (as a file whose thumbnail is generated on demand, or re-uploaded as an avatar), so the instance can be crashed again immediately after each restart; if supervised with auto-restart, the attacker scripts a sustained outage.**Amplification**: each request costs the attacker ~65 bytes but costs the server an attempted multi-GB/TB allocation; trivial to repeat/parallelize.**No special privilege**: any registered user (avatar). The thumbnail sink lets a planted bomb in a public share be re-triggered anonymously.Suggested MitigationCap **decoded pixel dimensions** before fully decoding, in addition to the existing file-size cap. Use `image.DecodeConfig` (reads only the header) and reject images whose `width × height` (or either dimension) exceeds a configurable limit; apply it in `NewThumbFromFile` so both the thumbnail and avatar paths are covered.Additionally:Apply the same pixel cap to the avatar path (covered automatically if it routes through `NewThumbFromFile`, as it does).Consider running image decoding in a memory-limited child process / with `debug.SetMemoryLimit` + a `GOMEMLIMIT`-aware soft fail so a single decode cannot take down the whole server even if a future decoder lacks a header pre-check.Why sufficient: `DecodeConfig` parses only the header (no large allocation), so the oversized buffer is never created; legitimate images decode unchanged.
CVSS v3.1 score: 6.5