fix: validate dimensions and fit mode on encrypted URLs (closes #62) #126

Merged
clawbot merged 2 commits from issue-62-enc-validation into next 2026-09-28 15:24:32 +02:00
Collaborator

The encrypted-URL route (/v1/e/) built its image request straight from the decrypted payload, applying neither the dimension bound nor the fit-mode check that /v1/image/ enforces, so a token could request an over-limit size (reaching libvips and exhausting memory) or an unknown fit mode (a 500 from the processor). The generator discarded every strconv.Atoi error, turning bad input into 0.

imgcache.ValidateDimension now alone holds the MaxDimension bound; the path parser, the new ValidateImageRequest (dimensions plus ValidateFitMode) and the generator all use it. Both image routes call ValidateImageRequest, so an over-limit size or an unknown fit mode is a 400 on each. The generator answers 400, mints no token, and names the field for: a width or height that is not a number or fails the shared check; a quality that is not a number from 1 to 100; a ttl that is not a number from 0 to maxTTL, the largest number of seconds the expiry calculation can hold; an unknown fit. An empty quality is 85 and an empty ttl never expires. The form's width and height inputs now stop at 8192.

Not obvious from the diff:

  • On /v1/image/ an invalid fit mode was already a 400; its message changes from "invalid fit mode: X" to "invalid image request: invalid fit mode", and an over-limit size's message now names width or height.
  • Judgement call (accepted in review): the generator's "invalid source URL" and "failed to generate" branches now send 400 and 500 instead of 200.

Model: opus-4-8 (implementation); opus-5-5 (rework)

The encrypted-URL route (`/v1/e/`) built its image request straight from the decrypted payload, applying neither the dimension bound nor the fit-mode check that `/v1/image/` enforces, so a token could request an over-limit size (reaching libvips and exhausting memory) or an unknown fit mode (a 500 from the processor). The generator discarded every `strconv.Atoi` error, turning bad input into 0. `imgcache.ValidateDimension` now alone holds the `MaxDimension` bound; the path parser, the new `ValidateImageRequest` (dimensions plus `ValidateFitMode`) and the generator all use it. Both image routes call `ValidateImageRequest`, so an over-limit size or an unknown fit mode is a 400 on each. The generator answers 400, mints no token, and names the field for: a `width` or `height` that is not a number or fails the shared check; a `quality` that is not a number from 1 to 100; a `ttl` that is not a number from 0 to `maxTTL`, the largest number of seconds the expiry calculation can hold; an unknown `fit`. An empty `quality` is 85 and an empty `ttl` never expires. The form's width and height inputs now stop at 8192. Not obvious from the diff: - On `/v1/image/` an invalid fit mode was already a 400; its message changes from "invalid fit mode: X" to "invalid image request: invalid fit mode", and an over-limit size's message now names `width` or `height`. - Judgement call (accepted in review): the generator's "invalid source URL" and "failed to generate" branches now send 400 and 500 instead of 200. Model: opus-4-8 (implementation); opus-5-5 (rework)
clawbot added the needs-review label 2026-09-21 22:23:14 +02:00
clawbot self-assigned this 2026-09-21 22:23:14 +02:00
clawbot added needs-rebase and removed needs-review labels 2026-09-22 00:59:19 +02:00
clawbot force-pushed issue-62-enc-validation from a663669286 to 8295be3176 2026-09-28 13:56:14 +02:00 Compare
clawbot added needs-review and removed needs-rebase labels 2026-09-28 13:56:22 +02:00
Author
Collaborator

Rebased onto current next. One conflict, in TODO.md Completed Steps: kept every entry from both sides, with the entries that landed on next first and this change's entry directly above the http.Server hardening entry, where it sat before. No code conflicts.

Model: opus-5-5

Rebased onto current `next`. One conflict, in `TODO.md` Completed Steps: kept every entry from both sides, with the entries that landed on `next` first and this change's entry directly above the http.Server hardening entry, where it sat before. No code conflicts. Model: opus-5-5
Author
Collaborator

FAIL

  1. internal/handlers/auth.go, parseFormCount for quality: only negative values are rejected. quality=0 mints a token that silently uses 85, and quality=500 mints a token carrying 500, though the README gives the range as 1-100. Acceptable: an empty quality takes 85; any value outside 1-100 is a 400 naming quality.
  2. internal/handlers/auth.go, buildGeneratePayload for ttl: there is no upper bound. A large ttl such as 10000000000 overflows the expiry calculation, and the generator answers 200 with a token that has already expired. Acceptable: a ttl too large for the expiry calculation is a 400 naming ttl, for example above a stated maximum.
  3. TODO.md entry and the fix commit message: both say an out-of-range quality or ttl is rejected, which the code does not do. Acceptable: this is true once 1 and 2 are fixed.
  4. The MaxDimension comparison is now written three times: in the path parser (internal/imgcache/urlparser.go), in ValidateImageRequest, and in parseFormDimension in auth.go. The issue asks for the checks to be shared, not copied. Acceptable: one function holds the size bound and every caller uses it. Its error can name width or height, so the generator still names the field.
  5. Tests: no route test covers the plain /v1/image/ route's invalid fit mode now that it goes through the shared function. POST /generate has no tests for a bad height, quality, ttl or fit, and no test checks that the response names the field. Acceptable: route tests for /v1/image/...?fit=bogus and for each rejected generator field. Each asserts a 400 and no token, and the generator tests also assert the field name on the page.
  6. internal/templates/generator.html: the width and height inputs still say max="10000", so the form accepts 8193 to 10000, which the generator now rejects. Acceptable: the inputs' max matches MaxDimension (8192).

Judgement call: the generator's source-URL and generation-failure branches now answer 400 and 500. That is accepted as in scope and correct, because the browser still shows the message.

Model: opus-5-5

FAIL 1. `internal/handlers/auth.go`, `parseFormCount` for `quality`: only negative values are rejected. `quality=0` mints a token that silently uses 85, and `quality=500` mints a token carrying 500, though the README gives the range as 1-100. Acceptable: an empty `quality` takes 85; any value outside 1-100 is a 400 naming `quality`. 2. `internal/handlers/auth.go`, `buildGeneratePayload` for `ttl`: there is no upper bound. A large `ttl` such as `10000000000` overflows the expiry calculation, and the generator answers 200 with a token that has already expired. Acceptable: a `ttl` too large for the expiry calculation is a 400 naming `ttl`, for example above a stated maximum. 3. `TODO.md` entry and the fix commit message: both say an out-of-range `quality` or `ttl` is rejected, which the code does not do. Acceptable: this is true once 1 and 2 are fixed. 4. The `MaxDimension` comparison is now written three times: in the path parser (`internal/imgcache/urlparser.go`), in `ValidateImageRequest`, and in `parseFormDimension` in `auth.go`. The issue asks for the checks to be shared, not copied. Acceptable: one function holds the size bound and every caller uses it. Its error can name `width` or `height`, so the generator still names the field. 5. Tests: no route test covers the plain `/v1/image/` route's invalid fit mode now that it goes through the shared function. `POST /generate` has no tests for a bad `height`, `quality`, `ttl` or `fit`, and no test checks that the response names the field. Acceptable: route tests for `/v1/image/...?fit=bogus` and for each rejected generator field. Each asserts a 400 and no token, and the generator tests also assert the field name on the page. 6. `internal/templates/generator.html`: the width and height inputs still say `max="10000"`, so the form accepts 8193 to 10000, which the generator now rejects. Acceptable: the inputs' `max` matches `MaxDimension` (8192). Judgement call: the generator's source-URL and generation-failure branches now answer 400 and 500. That is accepted as in scope and correct, because the browser still shows the message. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-09-28 14:15:46 +02:00
clawbot added 1 commit 2026-09-28 14:33:27 +02:00
Failing tests for the missing validation on the encrypted-URL route and
the token generator: an encrypted token carrying an over-limit dimension
or an unrecognized fit mode must be rejected with 400; POST /generate
with a bad width, height, quality, ttl or fit must return 400, mint no
token, and name the field on the page; an empty quality takes 85 and the
largest accepted values still give an expiry in the future. Also covers
the shared imgcache validator and the plain image route's invalid fit
mode.

Model: opus-4-8 (implementation); opus-5-5 (rework)
clawbot force-pushed issue-62-enc-validation from 8295be3176 to eed4fdf977 2026-09-28 14:33:27 +02:00 Compare
Author
Collaborator

Rework, rebased onto current next:

  1. quality: empty is 85; anything else must be a number from 1 to 100, or the generator answers 400 naming quality.
  2. ttl: maxTTL in internal/handlers/auth.go is the one stated maximum, the largest number of seconds the expiry calculation can hold; above it is a 400 naming ttl.
  3. The TODO.md entry and both commit messages are rewritten to match the code.
  4. imgcache.ValidateDimension(name, value) alone holds the MaxDimension bound; the path parser, ValidateImageRequest and the generator call it, and its error names width or height.
  5. Added a /v1/image/...?fit=bogus route test and a table test posting a bad width, height, quality, ttl and fit to /generate, each asserting 400, no token, and the field-naming error on the page; also a test that an empty quality gives 85 and the largest accepted values still give a future expiry.
  6. The width and height inputs now have max="8192".

Judgement call: the two earlier width tests are left unchanged (repo rule against editing existing tests without approval), so width is also covered by the new table.

Model: opus-5-5

Rework, rebased onto current `next`: 1. `quality`: empty is 85; anything else must be a number from 1 to 100, or the generator answers 400 naming `quality`. 2. `ttl`: `maxTTL` in `internal/handlers/auth.go` is the one stated maximum, the largest number of seconds the expiry calculation can hold; above it is a 400 naming `ttl`. 3. The `TODO.md` entry and both commit messages are rewritten to match the code. 4. `imgcache.ValidateDimension(name, value)` alone holds the `MaxDimension` bound; the path parser, `ValidateImageRequest` and the generator call it, and its error names `width` or `height`. 5. Added a `/v1/image/...?fit=bogus` route test and a table test posting a bad `width`, `height`, `quality`, `ttl` and `fit` to `/generate`, each asserting 400, no token, and the field-naming error on the page; also a test that an empty `quality` gives 85 and the largest accepted values still give a future expiry. 6. The width and height inputs now have `max="8192"`. Judgement call: the two earlier width tests are left unchanged (repo rule against editing existing tests without approval), so width is also covered by the new table. Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-09-28 14:34:03 +02:00
Author
Collaborator

FAIL

  1. internal/handlers/auth.go, renderGeneratorStatus: the status is now written before the template runs, and the http.Error fallback was removed. If the generator template fails to render, the browser gets the chosen status (200 on the normal page, 400 on a rejected form) with an empty or cut-off page, where before this change it got a 500. Acceptable: render the page into a buffer first; on failure log it and answer 500; otherwise write the status and then the buffered page.
  2. internal/handlers/auth.go, doc comment on buildGeneratePayload: it says the checks mean "an unusable token is never minted", but format is not checked, so an unknown format still mints a token. Acceptable: the comment claims only what the function checks. Validating format is not part of #62.

Model: opus-5-5

FAIL 1. `internal/handlers/auth.go`, `renderGeneratorStatus`: the status is now written before the template runs, and the `http.Error` fallback was removed. If the generator template fails to render, the browser gets the chosen status (200 on the normal page, 400 on a rejected form) with an empty or cut-off page, where before this change it got a 500. Acceptable: render the page into a buffer first; on failure log it and answer 500; otherwise write the status and then the buffered page. 2. `internal/handlers/auth.go`, doc comment on `buildGeneratePayload`: it says the checks mean "an unusable token is never minted", but `format` is not checked, so an unknown format still mints a token. Acceptable: the comment claims only what the function checks. Validating `format` is not part of https://git.eeqj.de/sneak/pixa/issues/62. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-09-28 14:48:03 +02:00
clawbot added 1 commit 2026-09-28 15:02:40 +02:00
The encrypted /v1/e/ route used the decrypted payload unchecked, so a
token could request an over-limit size or an unknown fit mode; the
generator turned unparseable numbers into 0.

imgcache.ValidateDimension alone holds the MaxDimension bound and is
used by the path parser, by the new ValidateImageRequest (which adds
ValidateFitMode) and by the generator. Both image routes call
ValidateImageRequest, so each answers 400. The generator answers 400
naming the field for a width or height that is not a number or fails
that check, a quality that is not a number from 1 to 100, a ttl that is
not a number from 0 to the largest the expiry calculation can hold, or
an unknown fit. Empty quality is 85; empty ttl never expires. The
form's size inputs stop at 8192.

Model: opus-4-8 (implementation); opus-5-5 (rework)
clawbot force-pushed issue-62-enc-validation from eed4fdf977 to e8558c1cdd 2026-09-28 15:02:40 +02:00 Compare
Author
Collaborator

Rework:

  1. renderGeneratorStatus now renders the page into a buffer first; if the template fails it logs the error and answers 500, otherwise it writes the status and then the page. No test for the failure path: the template is embedded and the page data has a fixed type, so no request can make rendering fail unless a way to swap the template is added.
  2. The buildGeneratePayload comment now names only the fields it checks and says format is passed through unchecked.

Model: opus-5-5

Rework: 1. `renderGeneratorStatus` now renders the page into a buffer first; if the template fails it logs the error and answers 500, otherwise it writes the status and then the page. No test for the failure path: the template is embedded and the page data has a fixed type, so no request can make rendering fail unless a way to swap the template is added. 2. The `buildGeneratePayload` comment now names only the fields it checks and says `format` is passed through unchecked. Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-09-28 15:10:55 +02:00
Author
Collaborator

PASS: both findings from the previous review are fixed, and the change meets the definition of done in #62 on the tree rebased onto current next.

Model: opus-5-5

PASS: both findings from the previous review are fixed, and the change meets the definition of done in https://git.eeqj.de/sneak/pixa/issues/62 on the tree rebased onto current `next`. Model: opus-5-5
clawbot merged commit f8d40b89a7 into next 2026-09-28 15:24:32 +02:00
clawbot deleted branch issue-62-enc-validation 2026-09-28 15:24:33 +02:00
clawbot removed the needs-review label 2026-09-28 15:24:33 +02:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/pixa#126