mirror of
https://github.com/golang/go.git
synced 2025-10-19 11:03:18 +00:00
net/url: enforce stricter parsing of bracketed IPv6 hostnames
- Previously, url.Parse did not enforce validation of hostnames within square brackets. - RFC 3986 stipulates that only IPv6 hostnames can be embedded within square brackets in a URL. - Now, the parsing logic should strictly enforce that only IPv6 hostnames can be resolved when in square brackets. IPv4, IPv4-mapped addresses and other input will be rejected. - Update url_test to add test cases that cover the above scenarios. Thanks to Enze Wang, Jingcheng Yang and Zehui Miao of Tsinghua University for reporting this issue. Fixes CVE-2025-47912 Fixes #75678 Change-Id: Iaa41432bf0ee86de95a39a03adae5729e4deb46c Reviewed-on: https://go-internal-review.googlesource.com/c/go/+/2680 Reviewed-by: Damien Neil <dneil@google.com> Reviewed-by: Roland Shoemaker <bracewell@google.com> Reviewed-on: https://go-review.googlesource.com/c/go/+/709857 TryBot-Bypass: Michael Pratt <mpratt@google.com> Reviewed-by: Carlos Amedee <carlos@golang.org> Auto-Submit: Michael Pratt <mpratt@google.com>
This commit is contained in:
parent
7dd54e1fd7
commit
f6f4e8b3ef
3 changed files with 77 additions and 14 deletions
|
@ -237,7 +237,6 @@ var depsRules = `
|
|||
internal/types/errors,
|
||||
mime/quotedprintable,
|
||||
net/internal/socktest,
|
||||
net/url,
|
||||
runtime/trace,
|
||||
text/scanner,
|
||||
text/tabwriter;
|
||||
|
@ -300,6 +299,12 @@ var depsRules = `
|
|||
FMT
|
||||
< text/template/parse;
|
||||
|
||||
internal/bytealg, internal/itoa, math/bits, slices, strconv, unique
|
||||
< net/netip;
|
||||
|
||||
FMT, net/netip
|
||||
< net/url;
|
||||
|
||||
net/url, text/template/parse
|
||||
< text/template
|
||||
< internal/lazytemplate;
|
||||
|
@ -414,9 +419,6 @@ var depsRules = `
|
|||
< golang.org/x/net/dns/dnsmessage,
|
||||
golang.org/x/net/lif;
|
||||
|
||||
internal/bytealg, internal/itoa, math/bits, slices, strconv, unique
|
||||
< net/netip;
|
||||
|
||||
os, net/netip
|
||||
< internal/routebsd;
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/netip"
|
||||
"path"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
@ -642,40 +643,61 @@ func parseAuthority(authority string) (user *Userinfo, host string, err error) {
|
|||
// parseHost parses host as an authority without user
|
||||
// information. That is, as host[:port].
|
||||
func parseHost(host string) (string, error) {
|
||||
if strings.HasPrefix(host, "[") {
|
||||
if openBracketIdx := strings.LastIndex(host, "["); openBracketIdx != -1 {
|
||||
// Parse an IP-Literal in RFC 3986 and RFC 6874.
|
||||
// E.g., "[fe80::1]", "[fe80::1%25en0]", "[fe80::1]:80".
|
||||
i := strings.LastIndex(host, "]")
|
||||
if i < 0 {
|
||||
closeBracketIdx := strings.LastIndex(host, "]")
|
||||
if closeBracketIdx < 0 {
|
||||
return "", errors.New("missing ']' in host")
|
||||
}
|
||||
colonPort := host[i+1:]
|
||||
|
||||
colonPort := host[closeBracketIdx+1:]
|
||||
if !validOptionalPort(colonPort) {
|
||||
return "", fmt.Errorf("invalid port %q after host", colonPort)
|
||||
}
|
||||
unescapedColonPort, err := unescape(colonPort, encodeHost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hostname := host[openBracketIdx+1 : closeBracketIdx]
|
||||
var unescapedHostname string
|
||||
// RFC 6874 defines that %25 (%-encoded percent) introduces
|
||||
// the zone identifier, and the zone identifier can use basically
|
||||
// any %-encoding it likes. That's different from the host, which
|
||||
// can only %-encode non-ASCII bytes.
|
||||
// We do impose some restrictions on the zone, to avoid stupidity
|
||||
// like newlines.
|
||||
zone := strings.Index(host[:i], "%25")
|
||||
if zone >= 0 {
|
||||
host1, err := unescape(host[:zone], encodeHost)
|
||||
zoneIdx := strings.Index(hostname, "%25")
|
||||
if zoneIdx >= 0 {
|
||||
hostPart, err := unescape(hostname[:zoneIdx], encodeHost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
host2, err := unescape(host[zone:i], encodeZone)
|
||||
zonePart, err := unescape(hostname[zoneIdx:], encodeZone)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
host3, err := unescape(host[i:], encodeHost)
|
||||
unescapedHostname = hostPart + zonePart
|
||||
} else {
|
||||
var err error
|
||||
unescapedHostname, err = unescape(hostname, encodeHost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return host1 + host2 + host3, nil
|
||||
}
|
||||
|
||||
// Per RFC 3986, only a host identified by a valid
|
||||
// IPv6 address can be enclosed by square brackets.
|
||||
// This excludes any IPv4 or IPv4-mapped addresses.
|
||||
addr, err := netip.ParseAddr(unescapedHostname)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid host: %w", err)
|
||||
}
|
||||
if addr.Is4() || addr.Is4In6() {
|
||||
return "", errors.New("invalid IPv6 host")
|
||||
}
|
||||
return "[" + unescapedHostname + "]" + unescapedColonPort, nil
|
||||
} else if i := strings.LastIndex(host, ":"); i != -1 {
|
||||
colonPort := host[i:]
|
||||
if !validOptionalPort(colonPort) {
|
||||
|
|
|
@ -383,6 +383,16 @@ var urltests = []URLTest{
|
|||
},
|
||||
"",
|
||||
},
|
||||
// valid IPv6 host with port and path
|
||||
{
|
||||
"https://[2001:db8::1]:8443/test/path",
|
||||
&URL{
|
||||
Scheme: "https",
|
||||
Host: "[2001:db8::1]:8443",
|
||||
Path: "/test/path",
|
||||
},
|
||||
"",
|
||||
},
|
||||
// host subcomponent; IPv6 address with zone identifier in RFC 6874
|
||||
{
|
||||
"http://[fe80::1%25en0]/", // alphanum zone identifier
|
||||
|
@ -707,6 +717,24 @@ var parseRequestURLTests = []struct {
|
|||
// RFC 6874.
|
||||
{"http://[fe80::1%en0]/", false},
|
||||
{"http://[fe80::1%en0]:8080/", false},
|
||||
|
||||
// Tests exercising RFC 3986 compliance
|
||||
{"https://[1:2:3:4:5:6:7:8]", true}, // full IPv6 address
|
||||
{"https://[2001:db8::a:b:c:d]", true}, // compressed IPv6 address
|
||||
{"https://[fe80::1%25eth0]", true}, // link-local address with zone ID (interface name)
|
||||
{"https://[fe80::abc:def%254]", true}, // link-local address with zone ID (interface index)
|
||||
{"https://[2001:db8::1]/path", true}, // compressed IPv6 address with path
|
||||
{"https://[fe80::1%25eth0]/path?query=1", true}, // link-local with zone, path, and query
|
||||
|
||||
{"https://[::ffff:192.0.2.1]", false},
|
||||
{"https://[:1] ", false},
|
||||
{"https://[1:2:3:4:5:6:7:8:9]", false},
|
||||
{"https://[1::1::1]", false},
|
||||
{"https://[1:2:3:]", false},
|
||||
{"https://[ffff::127.0.0.4000]", false},
|
||||
{"https://[0:0::test.com]:80", false},
|
||||
{"https://[2001:db8::test.com]", false},
|
||||
{"https://[test.com]", false},
|
||||
}
|
||||
|
||||
func TestParseRequestURI(t *testing.T) {
|
||||
|
@ -1643,6 +1671,17 @@ func TestParseErrors(t *testing.T) {
|
|||
{"cache_object:foo", true},
|
||||
{"cache_object:foo/bar", true},
|
||||
{"cache_object/:foo/bar", false},
|
||||
|
||||
{"http://[192.168.0.1]/", true}, // IPv4 in brackets
|
||||
{"http://[192.168.0.1]:8080/", true}, // IPv4 in brackets with port
|
||||
{"http://[::ffff:192.168.0.1]/", true}, // IPv4-mapped IPv6 in brackets
|
||||
{"http://[::ffff:192.168.0.1]:8080/", true}, // IPv4-mapped IPv6 in brackets with port
|
||||
{"http://[::ffff:c0a8:1]/", true}, // IPv4-mapped IPv6 in brackets (hex)
|
||||
{"http://[not-an-ip]/", true}, // invalid IP string in brackets
|
||||
{"http://[fe80::1%foo]/", true}, // invalid zone format in brackets
|
||||
{"http://[fe80::1", true}, // missing closing bracket
|
||||
{"http://fe80::1]/", true}, // missing opening bracket
|
||||
{"http://[test.com]/", true}, // domain name in brackets
|
||||
}
|
||||
for _, tt := range tests {
|
||||
u, err := Parse(tt.in)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue