The image routes (/v1/image/ and /v1/e/) have no limit per client. TODO.md names this as the next step ("P2: security: per-IP rate limiting on the image routes"), and no issue tracks it.
Why it is a defect: with allowlist_hosts set, anyone can request an allowlisted image at any size, format, quality and fit without a signature, and each new combination is fetched or read, decoded and encoded. max_concurrent_processing caps the work done at once, but one client can keep every processing slot busy, so every other client's cache misses wait behind it until their downstream_timeout ends.
Design (recommended, built unless the owner rules otherwise)
One new setting, image_requests_per_minute (PIXA_IMAGE_REQUESTS_PER_MINUTE): how many image requests one client may make per minute. Default 600. 0 turns the limit off. A negative or non-integer value stops pixad at startup, as other settings do.
The limit applies to GET and HEAD on both image routes, through the existing RateLimit middleware in internal/middleware/middleware.go, so a client is told apart exactly as for the login limit (trusted_proxies, an IPv6 client by its /64). A request over the limit gets 429 with Retry-After. Cache hits count too; the limit is about requests, not work.
A browser's CORS preflight (OPTIONS) is not counted.
README.md documents the setting next to the login limit, and says that behind a CDN or proxy whose addresses are not in trusted_proxies every request counts as the same client, so such a deployment raises the limit or sets 0. config.example.yml and the settings table name it.
Definition of done
The setting exists as above, read and validated like the other settings, with its default and 0 documented.
New tests, through the router: the request after the limit within a minute gets 429 with Retry-After; two clients are counted apart; 0 lets every request through; an invalid value stops startup. Existing tests are not changed (CLAUDE.md).
TODO.md: the next step moves to Completed Steps, and the next item of Future Steps becomes the next step.
script/cibuild passes; one PR against next, reviewed by an independent reviewer.
Model: opus-5-5
The image routes (`/v1/image/` and `/v1/e/`) have no limit per client. `TODO.md` names this as the next step ("P2: security: per-IP rate limiting on the image routes"), and no issue tracks it.
Why it is a defect: with `allowlist_hosts` set, anyone can request an allowlisted image at any size, format, quality and fit without a signature, and each new combination is fetched or read, decoded and encoded. `max_concurrent_processing` caps the work done at once, but one client can keep every processing slot busy, so every other client's cache misses wait behind it until their `downstream_timeout` ends.
## Design (recommended, built unless the owner rules otherwise)
- One new setting, `image_requests_per_minute` (`PIXA_IMAGE_REQUESTS_PER_MINUTE`): how many image requests one client may make per minute. Default 600. `0` turns the limit off. A negative or non-integer value stops pixad at startup, as other settings do.
- The limit applies to GET and HEAD on both image routes, through the existing `RateLimit` middleware in `internal/middleware/middleware.go`, so a client is told apart exactly as for the login limit (`trusted_proxies`, an IPv6 client by its /64). A request over the limit gets 429 with `Retry-After`. Cache hits count too; the limit is about requests, not work.
- A browser's CORS preflight (OPTIONS) is not counted.
- `README.md` documents the setting next to the login limit, and says that behind a CDN or proxy whose addresses are not in `trusted_proxies` every request counts as the same client, so such a deployment raises the limit or sets `0`. `config.example.yml` and the settings table name it.
## Definition of done
- The setting exists as above, read and validated like the other settings, with its default and `0` documented.
- New tests, through the router: the request after the limit within a minute gets 429 with `Retry-After`; two clients are counted apart; `0` lets every request through; an invalid value stops startup. Existing tests are not changed (`CLAUDE.md`).
- `TODO.md`: the next step moves to Completed Steps, and the next item of Future Steps becomes the next step.
- `script/cibuild` passes; one PR against `next`, reviewed by an independent reviewer.
Model: opus-5-5
clawbot
self-assigned this 2026-10-12 10:56:10 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The image routes (
/v1/image/and/v1/e/) have no limit per client.TODO.mdnames this as the next step ("P2: security: per-IP rate limiting on the image routes"), and no issue tracks it.Why it is a defect: with
allowlist_hostsset, anyone can request an allowlisted image at any size, format, quality and fit without a signature, and each new combination is fetched or read, decoded and encoded.max_concurrent_processingcaps the work done at once, but one client can keep every processing slot busy, so every other client's cache misses wait behind it until theirdownstream_timeoutends.Design (recommended, built unless the owner rules otherwise)
image_requests_per_minute(PIXA_IMAGE_REQUESTS_PER_MINUTE): how many image requests one client may make per minute. Default 600.0turns the limit off. A negative or non-integer value stops pixad at startup, as other settings do.RateLimitmiddleware ininternal/middleware/middleware.go, so a client is told apart exactly as for the login limit (trusted_proxies, an IPv6 client by its /64). A request over the limit gets 429 withRetry-After. Cache hits count too; the limit is about requests, not work.README.mddocuments the setting next to the login limit, and says that behind a CDN or proxy whose addresses are not intrusted_proxiesevery request counts as the same client, so such a deployment raises the limit or sets0.config.example.ymland the settings table name it.Definition of done
0documented.Retry-After; two clients are counted apart;0lets every request through; an invalid value stops startup. Existing tests are not changed (CLAUDE.md).TODO.md: the next step moves to Completed Steps, and the next item of Future Steps becomes the next step.script/cibuildpasses; one PR againstnext, reviewed by an independent reviewer.Model: opus-5-5