Описание
Cloudreve: Denial of Service - Image decompression / pixel bomb in thumbnail & avatar decoding crashes the server
Summary
Cloudreve'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.
Details
Root cause — decode guarded by file size, not pixel count
The 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 error
For 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; or
- the Go runtime
throw("out of memory")— a fatal error, not a recoverablepanic, sogin.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 Concept
The 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 result
Impact
- 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 Mitigation
Cap 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+ aGOMEMLIMIT-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.
Пакеты
github.com/cloudreve/Cloudreve/v4
< 4.0.0-20260613024411-3607f79bb44c
4.0.0-20260613024411-3607f79bb44c
github.com/cloudreve/Cloudreve/v3
<= 3.0.0-20250225100611-da4e44b77af4
Отсутствует
Связанные уязвимости
Cloudreve is a self-hosted file management and sharing system. Prior to 4.17.0, the built-in thumbnail and avatar image decoders limit compressed file size but do not limit decoded pixel dimensions, allowing an authenticated user to submit a small PNG, JPEG, or GIF that triggers an unbounded allocation and terminates the Cloudreve process through fatal out-of-memory behavior. This issue is fixed in version 4.17.0.