diff --git a/internal/imgcache/urlparser.go b/internal/imgcache/urlparser.go index d109025..467c8d9 100644 --- a/internal/imgcache/urlparser.go +++ b/internal/imgcache/urlparser.go @@ -3,6 +3,7 @@ package imgcache import ( "errors" "fmt" + "net/netip" "net/url" "regexp" "strconv" @@ -50,11 +51,13 @@ var sizeFormatRegex = regexp.MustCompile( // percentEscapeRegex matches one percent-escape, such as "%2e" 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 +// hostRegex matches the forms the host of an image URL may take: a host name +// or an IPv4 address (letters, digits, "-", "_" and "."), or an IPv6 address +// in brackets (hex digits, ":" and "."), either one optionally followed by ":" +// and a port, which may be empty. Its one group holds the text in brackets, +// which parseImageComponents also checks is an IPv6 address. var hostRegex = regexp.MustCompile( - `^([A-Za-z0-9_.-]+|\[[0-9A-Fa-f:.]+\])(:[0-9]*)?$`) + `^(?:[A-Za-z0-9_.-]+|\[([0-9A-Fa-f:.]+)\])(?::[0-9]*)?$`) // ParsedURL contains the parsed components of an image proxy URL. type ParsedURL struct { @@ -147,14 +150,22 @@ func parseImageComponents(remainder string) (*ParsedURL, error) { return nil, ErrMissingHost } - // Go's URL parser, which the allowlist check and the fetcher use, reads a - // host that hostRegex does not match as a different host, or refuses it: - // what comes before an "@" is a user name and password, and what comes - // after a "?" or "#" is a query or a fragment - if !hostRegex.MatchString(host) { + // Only the forms hostRegex describes are accepted. A "?", "#" or "@" in + // the host would make Go's URL parser, which the allowlist check and the + // fetcher use, read a different host from the one allowlisted and cached + hostMatch := hostRegex.FindStringSubmatch(host) + if hostMatch == nil { return nil, fmt.Errorf("%w: %q", ErrInvalidHost, host) } + // Go's URL parser refuses brackets around anything but an IPv6 address + if inBrackets := hostMatch[1]; inBrackets != "" { + addr, err := netip.ParseAddr(inBrackets) + if err != nil || !addr.Is6() { + return nil, fmt.Errorf("%w: %q", ErrInvalidHost, host) + } + } + // Host names do not depend on letter case, so the allowlist, the upstream // connection limits, the cache and the signature see one spelling of each // host