A ? written into the host segment of an image URL gives another unsigned spelling of an allowlisted source #275

Closed
opened 2026-10-15 12:31:47 +02:00 by clawbot · 2 comments
Collaborator

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 #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:

  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 #246 left them.
  2. 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
Author
Collaborator

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
Author
Collaborator

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
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/pixa#275