Refuse brackets around anything but an IPv6 address in an image URL host
check / check (push) Waiting to run

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
This commit is contained in:
2026-10-15 22:55:37 +00:00
parent 46ee9fa1ca
commit 42afa539de
+20 -9
View File
@@ -3,6 +3,7 @@ package imgcache
import ( import (
"errors" "errors"
"fmt" "fmt"
"net/netip"
"net/url" "net/url"
"regexp" "regexp"
"strconv" "strconv"
@@ -50,11 +51,13 @@ 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 // hostRegex matches the forms the host of an image URL may take: a host name
// (letters, digits, "-", "_" and "."), or an IPv6 address in brackets, either // or an IPv4 address (letters, digits, "-", "_" and "."), or an IPv6 address
// one optionally followed by ":" and a port, which may be empty // 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( 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. // ParsedURL contains the parsed components of an image proxy URL.
type ParsedURL struct { type ParsedURL struct {
@@ -147,14 +150,22 @@ func parseImageComponents(remainder string) (*ParsedURL, error) {
return nil, ErrMissingHost return nil, ErrMissingHost
} }
// Go's URL parser, which the allowlist check and the fetcher use, reads a // Only the forms hostRegex describes are accepted. A "?", "#" or "@" in
// host that hostRegex does not match as a different host, or refuses it: // the host would make Go's URL parser, which the allowlist check and the
// what comes before an "@" is a user name and password, and what comes // fetcher use, read a different host from the one allowlisted and cached
// after a "?" or "#" is a query or a fragment hostMatch := hostRegex.FindStringSubmatch(host)
if !hostRegex.MatchString(host) { if hostMatch == nil {
return nil, fmt.Errorf("%w: %q", ErrInvalidHost, host) 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 // 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 // connection limits, the cache and the signature see one spelling of each
// host // host