Where: internal/imgcache/urlparser.goparseImageComponents on next at c23585d: the host segment is everything before the first / of the wildcard, and only @ is refused in it; the source query is split from the path only. Found by the review of #266.
What is wrong: a ? written into the host segment as %3F reaches the parser decoded, so /v1/image/cdn.example.com%3Fx=1/a.jpg/100x100.jpg gives the host cdn.example.com?x=1. The source URL built from it is https://cdn.example.com?x=1/a.jpg, which Go reads as host cdn.example.com with the query x=1/a.jpg: the allowlist passes it, pixa fetches the host's root path with that query, and the result is cached under the host as written. Each new text after the ? is another unsigned spelling of one upstream request with its own cache keys, the problem #246 closed for letter case and ports.
How it fails: with a host allowlisted, an outsider varies the text after %3F to make pixa fetch and store the same upstream response under as many cache keys as it likes.
Severity: low
Definition of done:
parseImageComponents refuses with 400 a host segment holding anything a URL host cannot hold (at least ?, #, %, \ and whitespace, besides @); a host name, an IPv4 address, a bracketed IPv6 address and a : with a port stay accepted, as #246 left them.
Test first for %3F and # in the host segment; make check green.
Package: internal/imgcache.
Model: opus-5-5
Where: `internal/imgcache/urlparser.go` `parseImageComponents` on `next` at `c23585d`: the host segment is everything before the first `/` of the wildcard, and only `@` is refused in it; the source query is split from the path only. Found by the review of https://git.eeqj.de/sneak/pixa/pulls/266.
What is wrong: a `?` written into the host segment as `%3F` reaches the parser decoded, so `/v1/image/cdn.example.com%3Fx=1/a.jpg/100x100.jpg` gives the host `cdn.example.com?x=1`. The source URL built from it is `https://cdn.example.com?x=1/a.jpg`, which Go reads as host `cdn.example.com` with the query `x=1/a.jpg`: the allowlist passes it, pixa fetches the host's root path with that query, and the result is cached under the host as written. Each new text after the `?` is another unsigned spelling of one upstream request with its own cache keys, the problem https://git.eeqj.de/sneak/pixa/issues/246 closed for letter case and ports.
How it fails: with a host allowlisted, an outsider varies the text after `%3F` to make pixa fetch and store the same upstream response under as many cache keys as it likes.
Severity: low
Definition of done:
1. `parseImageComponents` refuses with 400 a host segment holding anything a URL host cannot hold (at least `?`, `#`, `%`, `\` and whitespace, besides `@`); a host name, an IPv4 address, a bracketed IPv6 address and a `:` with a port stay accepted, as https://git.eeqj.de/sneak/pixa/issues/246 left them.
2. Test first for `%3F` and `#` in the host segment; `make check` green.
Package: `internal/imgcache`.
Model: opus-5-5
clawbot
self-assigned this 2026-10-15 12:31:47 +02:00
Plan: parseImageComponents in internal/imgcache/urlparser.go will say what the host of a /v1/image/ URL may be, in place of the one check for @: a host name or an IPv4 address (letters, digits, -, _ and .), or an IPv6 address in brackets, either one optionally followed by : and a port. Anything else is refused with 400, which covers ?, #, %, \, whitespace and @.
The underscore is there because allowlist_hosts accepts host names holding one, and such a host has to stay reachable. A : with no port stays accepted and still needs a signature, as #246 left it. An IPv6 address written without brackets will be refused with 400; until now it needed a signature.
The tests go through the router first: a ? written as %3F, a # written as %23, and the other characters, each refused with 400 on an allowlisted host. A second test checks that a host name, an IPv4 address, a bracketed IPv6 address, and a host with a port, or with a : and no port, are still served. The README line that now says only that a host holding @ is refused will say what a host may hold.
Model: opus-5-5
Plan: `parseImageComponents` in `internal/imgcache/urlparser.go` will say what the host of a `/v1/image/` URL may be, in place of the one check for `@`: a host name or an IPv4 address (letters, digits, `-`, `_` and `.`), or an IPv6 address in brackets, either one optionally followed by `:` and a port. Anything else is refused with 400, which covers `?`, `#`, `%`, `\`, whitespace and `@`.
The underscore is there because `allowlist_hosts` accepts host names holding one, and such a host has to stay reachable. A `:` with no port stays accepted and still needs a signature, as https://git.eeqj.de/sneak/pixa/issues/246 left it. An IPv6 address written without brackets will be refused with 400; until now it needed a signature.
The tests go through the router first: a `?` written as `%3F`, a `#` written as `%23`, and the other characters, each refused with 400 on an allowlisted host. A second test checks that a host name, an IPv4 address, a bracketed IPv6 address, and a host with a port, or with a `:` and no port, are still served. The README line that now says only that a host holding `@` is refused will say what a host may hold.
Model: opus-5-5
Built as planned above in #285. One point the plan did not name: a host name written in letters outside ASCII is refused too, and its punycode form is accepted.
Model: opus-5-5
Built as planned above in https://git.eeqj.de/sneak/pixa/pulls/285. One point the plan did not name: a host name written in letters outside ASCII is refused too, and its punycode form is accepted.
Model: opus-5-5
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.
Where:
internal/imgcache/urlparser.goparseImageComponentsonnextatc23585d: the host segment is everything before the first/of the wildcard, and only@is refused in it; the source query is split from the path only. Found by the review of #266.What is wrong: a
?written into the host segment as%3Freaches the parser decoded, so/v1/image/cdn.example.com%3Fx=1/a.jpg/100x100.jpggives the hostcdn.example.com?x=1. The source URL built from it ishttps://cdn.example.com?x=1/a.jpg, which Go reads as hostcdn.example.comwith the queryx=1/a.jpg: the allowlist passes it, pixa fetches the host's root path with that query, and the result is cached under the host as written. Each new text after the?is another unsigned spelling of one upstream request with its own cache keys, the problem #246 closed for letter case and ports.How it fails: with a host allowlisted, an outsider varies the text after
%3Fto make pixa fetch and store the same upstream response under as many cache keys as it likes.Severity: low
Definition of done:
parseImageComponentsrefuses with 400 a host segment holding anything a URL host cannot hold (at least?,#,%,\and whitespace, besides@); a host name, an IPv4 address, a bracketed IPv6 address and a:with a port stay accepted, as #246 left them.%3Fand#in the host segment;make checkgreen.Package:
internal/imgcache.Model: opus-5-5
Plan:
parseImageComponentsininternal/imgcache/urlparser.gowill say what the host of a/v1/image/URL may be, in place of the one check for@: a host name or an IPv4 address (letters, digits,-,_and.), or an IPv6 address in brackets, either one optionally followed by:and a port. Anything else is refused with 400, which covers?,#,%,\, whitespace and@.The underscore is there because
allowlist_hostsaccepts host names holding one, and such a host has to stay reachable. A:with no port stays accepted and still needs a signature, as #246 left it. An IPv6 address written without brackets will be refused with 400; until now it needed a signature.The tests go through the router first: a
?written as%3F, a#written as%23, and the other characters, each refused with 400 on an allowlisted host. A second test checks that a host name, an IPv4 address, a bracketed IPv6 address, and a host with a port, or with a:and no port, are still served. The README line that now says only that a host holding@is refused will say what a host may hold.Model: opus-5-5
Built as planned above in #285. One point the plan did not name: a host name written in letters outside ASCII is refused too, and its punycode form is accepted.
Model: opus-5-5