Only @ was refused in the host of a /v1/image/ URL, so a ? written as %3F gave the host cdn.example.com?x=1, which Go's URL parser reads as cdn.example.com with a query: it passed the allowlist and each text after the ? was cached under keys of its own. The host must now be 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. An IPv6 address without brackets, which needed a signature, is refused too. Model: opus-5-5
This commit is contained in:
@@ -575,7 +575,11 @@ pixa lower-cases the host of a `/v1/image/` URL before anything else uses it, so
|
|||||||
`CDN.Example.com` is matched, fetched, cached and counted against
|
`CDN.Example.com` is matched, fetched, cached and counted against
|
||||||
`upstream_connections_per_host` as `cdn.example.com`. Such a URL whose host has
|
`upstream_connections_per_host` as `cdn.example.com`. Such a URL whose host has
|
||||||
a port, as in `cdn.example.com:8443`, or a `:` with no port, needs a signature
|
a port, as in `cdn.example.com:8443`, or a `:` with no port, needs a signature
|
||||||
even when the host is allowlisted. A host holding `@` is refused with 400.
|
even when the host is allowlisted. The host must be a host name or an IPv4
|
||||||
|
address (letters, digits, hyphens, underscores and dots) or an IPv6 address in
|
||||||
|
brackets, either one optionally followed by `:` and a port. Any other host, such
|
||||||
|
as one holding `@`, or a `?`, `#`, `%`, `\` or space written percent-encoded, is
|
||||||
|
refused with 400.
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,17 @@ P2: security: per-origin rate limiting
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-15 the host of a `/v1/image/` URL must be a host name or an IPv4
|
||||||
|
address (letters, digits, hyphens, underscores and dots) or an IPv6 address in
|
||||||
|
brackets, either one optionally followed by `:` and a port, and any other host
|
||||||
|
is refused with 400 (closes #275): only `@` was refused, so a `?` written as
|
||||||
|
`%3F` gave the host `cdn.example.com?x=1`, which Go's URL parser reads as
|
||||||
|
`cdn.example.com` with a query. Such a URL passed the allowlist, and each text
|
||||||
|
after the `?` fetched the host's root path and cached the answer under keys of
|
||||||
|
its own. A `#`, `%`, `\` or space written percent-encoded is refused the same
|
||||||
|
way, and so is an IPv6 address without brackets, which needed a signature
|
||||||
|
before. Underscores are kept because `allowlist_hosts` accepts host names
|
||||||
|
holding them.
|
||||||
- 2026-10-15 the README says how a source URL with a query is written into an
|
- 2026-10-15 the README says how a source URL with a query is written into an
|
||||||
image URL, and the image route accepts it (closes #255): the query follows the
|
image URL, and the image route accepts it (closes #255): the query follows the
|
||||||
source path after `%3F`. pixa now reads the image URL's path decoded once, so
|
source path after `%3F`. pixa now reads the image URL's path decoded once, so
|
||||||
|
|||||||
@@ -50,6 +50,12 @@ var sizeFormatRegex = regexp.MustCompile(
|
|||||||
// percentEscapeRegex matches one percent-escape, such as "%2e"
|
// percentEscapeRegex matches one percent-escape, such as "%2e"
|
||||||
var percentEscapeRegex = regexp.MustCompile(`%[0-9A-Fa-f]{2}`)
|
var percentEscapeRegex = regexp.MustCompile(`%[0-9A-Fa-f]{2}`)
|
||||||
|
|
||||||
|
// hostRegex matches the host of an image URL: a host name or an IPv4 address
|
||||||
|
// (letters, digits, "-", "_" and "."), or an IPv6 address in brackets, either
|
||||||
|
// one optionally followed by ":" and a port, which may be empty
|
||||||
|
var hostRegex = regexp.MustCompile(
|
||||||
|
`^([A-Za-z0-9_.-]+|\[[0-9A-Fa-f:.]+\])(:[0-9]*)?$`)
|
||||||
|
|
||||||
// ParsedURL contains the parsed components of an image proxy URL.
|
// ParsedURL contains the parsed components of an image proxy URL.
|
||||||
type ParsedURL struct {
|
type ParsedURL struct {
|
||||||
// Host is the source origin host (e.g., "cdn.example.com")
|
// Host is the source origin host (e.g., "cdn.example.com")
|
||||||
@@ -141,10 +147,12 @@ func parseImageComponents(remainder string) (*ParsedURL, error) {
|
|||||||
return nil, ErrMissingHost
|
return nil, ErrMissingHost
|
||||||
}
|
}
|
||||||
|
|
||||||
// A URL reads what comes before "@" as a user name and password, and the
|
// Go's URL parser, which the allowlist check and the fetcher use, reads a
|
||||||
// rest as the host
|
// host that hostRegex does not match as a different host, or refuses it:
|
||||||
if strings.Contains(host, "@") {
|
// what comes before an "@" is a user name and password, and what comes
|
||||||
return nil, fmt.Errorf("%w: %q holds @", ErrInvalidHost, host)
|
// after a "?" or "#" is a query or a fragment
|
||||||
|
if !hostRegex.MatchString(host) {
|
||||||
|
return nil, fmt.Errorf("%w: %q", ErrInvalidHost, host)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Host names do not depend on letter case, so the allowlist, the upstream
|
// Host names do not depend on letter case, so the allowlist, the upstream
|
||||||
|
|||||||
Reference in New Issue
Block a user