Refuse an image URL whose host is not a host name or IP address (closes #275) #285

Merged
clawbot merged 4 commits from issue-275-host-segment-characters into next 2026-10-16 01:14:51 +02:00
Showing only changes of commit 42afa539de - Show all commits
+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