2 Commits
Author SHA1 Message Date
sneak 35c576e677 Refuse a q outside 1-100 on /v1/image/ with 400 (closes #134)
check / check (push) Failing after 58s
A q that was not a number or was outside 1-100 was dropped and 85 used,
so q=500 was served and verified against a signature made for 85. It is
now a 400 naming q and the value, read with the generator's quality
check; only a q missing from the URL is 85.

The route also refuses with 400 a query string that cannot be decoded
(r.URL.Query() drops such a pair, so q=80% arrived as no q) and any
parameter given more than once, which was read from its first value only
(q=80&q=500 was served at 80).

Model: opus-5-5
2026-09-28 15:19:12 +00:00
sneak 2dff1b5515 test: /v1/image/ answers an invalid q with 400 (closes #134)
Route tests: a q that is not a whole number from 1 to 100, an empty one
included, is a 400 naming q and the value; a parameter given more than
once is a 400 naming it; a query string that cannot be decoded is a 400
showing it. On next each is served, at 85 or at the first value given.

Model: opus-5-5
2026-09-28 15:19:12 +00:00
4 changed files with 30 additions and 9 deletions
+3
View File
@@ -94,6 +94,9 @@ In-process caching of request-to-output mappings targets 1-5k r/s.
Images are only fetched from origins using TLS with valid certificates. Images are only fetched from origins using TLS with valid certificates.
A request whose query string cannot be decoded, or gives any parameter more
than once, is refused with 400.
- `<format>`: one of `orig`, `png`, `jpeg`, `webp` - `<format>`: one of `orig`, `png`, `jpeg`, `webp`
- `<size>`: `orig` or `<width>x<height>` (e.g. `800x600`) - `<size>`: `orig` or `<width>x<height>` (e.g. `800x600`)
+4 -1
View File
@@ -36,7 +36,10 @@ exhaustion
85; the route reads `q` with the generator's quality check 85; the route reads `q` with the generator's quality check
(`parseFormInt` with `minQuality` and `maxQuality`); only a `q` missing (`parseFormInt` with `minQuality` and `maxQuality`); only a `q` missing
from the URL is still 85; a query string that cannot be decoded, such from the URL is still 85; a query string that cannot be decoded, such
as `q=80%`, is a 400 showing it; `README.md` states the range. as `q=80%`, is a 400 showing it; any query parameter given more than
once (`q`, `fit`, `sig`, `exp` alike) is a 400 naming it, so none is
read from its first value only; `README.md` states the range and both
query-string rules.
- 2026-09-28 unknown `PIXA_` environment variables abort startup (closes - 2026-09-28 unknown `PIXA_` environment variables abort startup (closes
#133): a variable whose name starts with `PIXA_` but is neither a #133): a variable whose name starts with `PIXA_` but is neither a
setting's variable nor `PIXA_CONFIG_PATH` aborts startup naming it, as setting's variable nor `PIXA_CONFIG_PATH` aborts startup naming it, as
+11 -7
View File
@@ -293,12 +293,13 @@ func TestHandleImage_InvalidFitMode_Returns400(t *testing.T) {
} }
} }
// TestHandleImage_InvalidQuality_Returns400 verifies that the plain image // TestHandleImage_InvalidQuery_Returns400 verifies that the plain image route
// route answers a q that is not a whole number from 1 to 100, an empty one // answers a q that is not a whole number from 1 to 100, an empty one
// included, with 400 naming q and the value, and a query string that cannot // included, with 400 naming q and the value, a parameter given more than once
// be decoded with 400 showing it, instead of serving the image at the default // with 400 naming it, and a query string that cannot be decoded with 400
// quality 85. // showing it, instead of serving the image at the default quality 85 or at
func TestHandleImage_InvalidQuality_Returns400(t *testing.T) { // the first value given.
func TestHandleImage_InvalidQuery_Returns400(t *testing.T) {
t.Parallel() t.Parallel()
tests := []struct { tests := []struct {
@@ -308,7 +309,10 @@ func TestHandleImage_InvalidQuality_Returns400(t *testing.T) {
{"q=0", `invalid q: must be from 1 to 100, got "0"`}, {"q=0", `invalid q: must be from 1 to 100, got "0"`},
{"q=101", `invalid q: must be from 1 to 100, got "101"`}, {"q=101", `invalid q: must be from 1 to 100, got "101"`},
{"q=", `invalid q: not a number, got ""`}, {"q=", `invalid q: not a number, got ""`},
{"q=&q=500", `invalid q: not a number, got ""`}, {"q=&q=500", `invalid q: given more than once`},
{"q=80&q=500", `invalid q: given more than once`},
{"q=80&q=", `invalid q: given more than once`},
{"fit=cover&fit=contain", `invalid fit: given more than once`},
{"q=80%", `invalid query string "q=80%": invalid URL escape "%"`}, {"q=80%", `invalid query string "q=80%": invalid URL escape "%"`},
{ {
"q=50;fit=contain", "q=50;fit=contain",
+12 -1
View File
@@ -94,7 +94,9 @@ func (s *Handlers) parseImageRequest(
// Parse signature params from query string. r.URL.Query() would silently // Parse signature params from query string. r.URL.Query() would silently
// drop a pair it cannot decode, such as q=80%, so that q would be served // drop a pair it cannot decode, such as q=80%, so that q would be served
// at 85; a query string that cannot be decoded is refused instead. // at 85; a query string that cannot be decoded is refused instead. A
// parameter given more than once is refused too, as only its first value
// would be read.
query, err := url.ParseQuery(r.URL.RawQuery) query, err := url.ParseQuery(r.URL.RawQuery)
if err != nil { if err != nil {
s.respondError(w, fmt.Sprintf("invalid query string %q: %v", s.respondError(w, fmt.Sprintf("invalid query string %q: %v",
@@ -103,6 +105,15 @@ func (s *Handlers) parseImageRequest(
return nil, false return nil, false
} }
for name, values := range query {
if len(values) > 1 {
s.respondError(w, fmt.Sprintf("invalid %s: given more than once",
name), http.StatusBadRequest)
return nil, false
}
}
req.Signature = query.Get("sig") req.Signature = query.Get("sig")
if expStr := query.Get("exp"); expStr != "" { if expStr := query.Get("exp"); expStr != "" {