From 42afa539de072aa8d77034fe8e25994ebad354ea Mon Sep 17 00:00:00 2001 From: sneak Date: Thu, 15 Oct 2026 22:55:37 +0000 Subject: [PATCH] Refuse brackets around anything but an IPv6 address in an image URL host The host check accepted any hex digits, : and . in brackets, so a host such as [203.0.113.5] or [abc] got through it, and Go's URL parser then refused the source URL, which the route answered with 401. The text in brackets must now parse as an IPv6 address, as Go's URL parser asks, or the host is refused with 400. The comment above the check now says only what is true of it. Model: opus-5-5 --- internal/imgcache/urlparser.go | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) 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