Refuse an image URL whose host is not a host name or IP address (closes #275) #285
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user