A request with one dimension 0 makes an output of unbounded size #241

Closed
opened 2026-10-13 08:46:08 +02:00 by clawbot · 2 comments
Collaborator

Where: internal/imageprocessor/imageprocessor.go lines 300-314 (targetDimensions) and 263-271 (Process), on v1.0.0-rc1.

What is wrong: ValidateDimension (internal/imgcache/urlparser.go line 28) caps only the numbers written in the request. When one of them is 0, targetDimensions computes the other side from the source's proportions, and nothing checks the result against the 8192 cap. resize then scales the image up to it: Thumbnail is called with libvips' size setting "both", which enlarges.

How it fails: on any deployment with an allowlisted host, an outsider requests /v1/image/<host>/<a wide image>/0x8192.jxl. A 3000x600 banner becomes a 40960x8192 output, 335 million pixels, about 1 GB to hold and minutes to encode; an 8192x1 source gives a width of 67 million pixels. The encode runs to the end whatever the request's deadline (the deadline bounds only the wait for a slot) and holds one of the max_concurrent_processing slots throughout; a few dozen such requests a minute, well under image_requests_per_minute, take every slot and gigabytes of memory. Containers on the shared host run without a memory limit by the owner's ruling (#158), so this lands on the host. With a signed or encrypted URL the maker chooses the size, so the exposure is the allowlist case.

Severity: critical

Definition of done:

  1. After the proportional computation, the output width and height are each at most MaxDimension; a request whose computed side exceeds it answers 400, or the side is clamped: say which in README.md under "Routes" and "Encrypted URLs".
  2. A failing test first: 0x8192 against a 3000x600 source is refused or clamped; orig and sizes with both numbers given are unchanged.
  3. make check green.

Model: fable-5-1

Where: `internal/imageprocessor/imageprocessor.go` lines 300-314 (`targetDimensions`) and 263-271 (`Process`), on `v1.0.0-rc1`. What is wrong: `ValidateDimension` (`internal/imgcache/urlparser.go` line 28) caps only the numbers written in the request. When one of them is `0`, `targetDimensions` computes the other side from the source's proportions, and nothing checks the result against the 8192 cap. `resize` then scales the image up to it: `Thumbnail` is called with libvips' size setting "both", which enlarges. How it fails: on any deployment with an allowlisted host, an outsider requests `/v1/image/<host>/<a wide image>/0x8192.jxl`. A 3000x600 banner becomes a 40960x8192 output, 335 million pixels, about 1 GB to hold and minutes to encode; an 8192x1 source gives a width of 67 million pixels. The encode runs to the end whatever the request's deadline (the deadline bounds only the wait for a slot) and holds one of the `max_concurrent_processing` slots throughout; a few dozen such requests a minute, well under `image_requests_per_minute`, take every slot and gigabytes of memory. Containers on the shared host run without a memory limit by the owner's ruling (https://git.eeqj.de/sneak/pixa/issues/158), so this lands on the host. With a signed or encrypted URL the maker chooses the size, so the exposure is the allowlist case. Severity: critical Definition of done: 1. After the proportional computation, the output width and height are each at most `MaxDimension`; a request whose computed side exceeds it answers 400, or the side is clamped: say which in `README.md` under "Routes" and "Encrypted URLs". 2. A failing test first: `0x8192` against a 3000x600 source is refused or clamped; `orig` and sizes with both numbers given are unchanged. 3. `make check` green. Model: fable-5-1
clawbot added the critical label 2026-10-13 08:46:08 +02:00
clawbot self-assigned this 2026-10-13 08:46:08 +02:00
Author
Collaborator

Decided: refuse, not clamp. A size whose side computed from the source's proportions exceeds the maximum answers 400 on both image routes, before anything is encoded, and is not cached as a failed fetch. Clamping would give the client something other than what it asked for, which CLAUDE.md forbids. Being fixed now, as the one critical finding of the v1.0.0-rc1 audit.

Model: opus-5-5

Decided: refuse, not clamp. A size whose side computed from the source's proportions exceeds the maximum answers 400 on both image routes, before anything is encoded, and is not cached as a failed fetch. Clamping would give the client something other than what it asked for, which `CLAUDE.md` forbids. Being fixed now, as the one critical finding of the `v1.0.0-rc1` audit. Model: opus-5-5
Author
Collaborator

Fixed in #258: a size with one side 0 whose computed side would be above 8192 is refused with 400 on both image routes, before the resize and the encode, and is not cached as a failed fetch, so other sizes of the same source are still served. fit=outside with both sides given can overshoot 8192 the same way; that is filed as #257.

Model: opus-5-5

Fixed in https://git.eeqj.de/sneak/pixa/pulls/258: a size with one side `0` whose computed side would be above 8192 is refused with 400 on both image routes, before the resize and the encode, and is not cached as a failed fetch, so other sizes of the same source are still served. `fit=outside` with both sides given can overshoot 8192 the same way; that is filed as https://git.eeqj.de/sneak/pixa/issues/257. Model: opus-5-5
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/pixa#241