Skip to content

Commit c1d9ca7

Browse files
committed
[release-branch.go1.11-security] net/url: make Hostname and Port predictable for invalid Host values
When Host is not valid per RFC 3986, the behavior of Hostname and Port was wildly unpredictable, to the point that Host could have a suffix that didn't appear in neither Hostname nor Port. This is a security issue when applications are applying checks to Host and expecting them to be meaningful for the contents of Hostname. To reduce disruption, this change only aims to guarantee the following two security-relevant invariants. * Host is either Hostname or [Hostname] with Port empty, or Hostname:Port or [Hostname]:Port. * Port is only decimals. The second invariant is the one that's most likely to cause disruption, but I believe it's important, as it's conceivable an application might do a suffix check on Host and expect it to be meaningful for the contents of Hostname (if the suffix is not a valid port). There are three ways to ensure it. 1) Reject invalid ports in Parse. Note that non-numeric ports are already rejected if and only if the host starts with "[". 2) Consider non-numeric ports as part of Hostname, not Port. 3) Allow non-numeric ports, and hope they only flow down to net/http, which will reject them (#14353). This change adopts both 1 and 2. We could do only the latter, but then these invalid hosts would flow past port checks, like in http_test.TestTransportRejectsAlphaPort. Non-numeric ports weren't fully supported anyway, because they were rejected after IPv6 literals, so this restores consistency. We could do only the former, but at this point 2) is free and might help with manually constructed Host values (or if we get something wrong in Parse). Note that net.SplitHostPort and net.Dial explicitly accept service names in place of port numbers, but this is an URL package, and RFC 3986, Section 3.2.3, clearly specifies ports as a number in decimal. net/http uses a mix of net.SplitHostPort and url.Parse that would deserve looking into, but in general it seems that it will still accept service names in Addr fields as they are passed to net.Listen, while rejecting them in URLs, which feels correct. This leaves a number of invalid URLs to reject, which however are not security relevant once the two invariants above hold, so can be done in Go 1.14: IPv6 literals without brackets (#31024), invalid IPv6 literals, hostnames with invalid characters, and more. Tested with 200M executions of go-fuzz and the following Fuzz function. u, err := url.Parse(string(data)) if err != nil { return 0 } h := u.Hostname() p := u.Port() switch u.Host { case h + ":" + p: return 1 case "[" + h + "]:" + p: return 1 case h: fallthrough case "[" + h + "]": if p != "" { panic("unexpected Port()") } return 1 } panic("Host is not a variant of [Hostname]:Port") Fixes CVE-2019-14809 Updates #29098 Change-Id: I7ef40823dab28f29511329fa2d5a7fb10c3ec895 Reviewed-on: https://go-review.googlesource.com/c/go/+/189258 Reviewed-by: Ian Lance Taylor <[email protected]> (cherry picked from commit 61bb56a) Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/526408 Reviewed-by: Dmitri Shuralyov <[email protected]> (cherry picked from commit 3226f2d) Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/526409
1 parent e152b01 commit c1d9ca7

File tree

4 files changed

+69
-65
lines changed

4 files changed

+69
-65
lines changed

src/net/http/transport.go

+2
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,8 @@ func resetProxyConfig() {
640640
}
641641

642642
func (t *Transport) connectMethodForRequest(treq *transportRequest) (cm connectMethod, err error) {
643+
// TODO: the validPort check is redundant after CL 189258, as url.URL.Port
644+
// only returns valid ports now. golang.org/issue/33600
643645
if port := treq.URL.Port(); !validPort(port) {
644646
return cm, fmt.Errorf("invalid URL port %q", port)
645647
}

src/net/http/transport_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -4111,7 +4111,7 @@ func TestTransportRejectsAlphaPort(t *testing.T) {
41114111
t.Fatalf("got %#v; want *url.Error", err)
41124112
}
41134113
got := ue.Err.Error()
4114-
want := `invalid URL port "123foo"`
4114+
want := `invalid port ":123foo" after host`
41154115
if got != want {
41164116
t.Errorf("got error %q; want %q", got, want)
41174117
}

src/net/url/url.go

+27-27
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,11 @@ func parseHost(host string) (string, error) {
636636
}
637637
return host1 + host2 + host3, nil
638638
}
639+
} else if i := strings.LastIndex(host, ":"); i != -1 {
640+
colonPort := host[i:]
641+
if !validOptionalPort(colonPort) {
642+
return "", fmt.Errorf("invalid port %q after host", colonPort)
643+
}
639644
}
640645

641646
var err error
@@ -1033,44 +1038,39 @@ func (u *URL) RequestURI() string {
10331038
return result
10341039
}
10351040

1036-
// Hostname returns u.Host, without any port number.
1041+
// Hostname returns u.Host, stripping any valid port number if present.
10371042
//
1038-
// If Host is an IPv6 literal with a port number, Hostname returns the
1039-
// IPv6 literal without the square brackets. IPv6 literals may include
1040-
// a zone identifier.
1043+
// If the result is enclosed in square brackets, as literal IPv6 addresses are,
1044+
// the square brackets are removed from the result.
10411045
func (u *URL) Hostname() string {
1042-
return stripPort(u.Host)
1046+
host, _ := splitHostPort(u.Host)
1047+
return host
10431048
}
10441049

10451050
// Port returns the port part of u.Host, without the leading colon.
1046-
// If u.Host doesn't contain a port, Port returns an empty string.
1051+
//
1052+
// If u.Host doesn't contain a valid numeric port, Port returns an empty string.
10471053
func (u *URL) Port() string {
1048-
return portOnly(u.Host)
1054+
_, port := splitHostPort(u.Host)
1055+
return port
10491056
}
10501057

1051-
func stripPort(hostport string) string {
1052-
colon := strings.IndexByte(hostport, ':')
1053-
if colon == -1 {
1054-
return hostport
1055-
}
1056-
if i := strings.IndexByte(hostport, ']'); i != -1 {
1057-
return strings.TrimPrefix(hostport[:i], "[")
1058-
}
1059-
return hostport[:colon]
1060-
}
1058+
// splitHostPort separates host and port. If the port is not valid, it returns
1059+
// the entire input as host, and it doesn't check the validity of the host.
1060+
// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
1061+
func splitHostPort(hostport string) (host, port string) {
1062+
host = hostport
10611063

1062-
func portOnly(hostport string) string {
1063-
colon := strings.IndexByte(hostport, ':')
1064-
if colon == -1 {
1065-
return ""
1066-
}
1067-
if i := strings.Index(hostport, "]:"); i != -1 {
1068-
return hostport[i+len("]:"):]
1064+
colon := strings.LastIndexByte(host, ':')
1065+
if colon != -1 && validOptionalPort(host[colon:]) {
1066+
host, port = host[:colon], host[colon+1:]
10691067
}
1070-
if strings.Contains(hostport, "]") {
1071-
return ""
1068+
1069+
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
1070+
host = host[1 : len(host)-1]
10721071
}
1073-
return hostport[colon+len(":"):]
1072+
1073+
return
10741074
}
10751075

10761076
// Marshaling interface implementations.

src/net/url/url_test.go

+39-37
Original file line numberDiff line numberDiff line change
@@ -422,10 +422,10 @@ var urltests = []URLTest{
422422
},
423423
// worst case host, still round trips
424424
{
425-
"scheme://!$&'()*+,;=hello!:port/path",
425+
"scheme://!$&'()*+,;=hello!:1/path",
426426
&URL{
427427
Scheme: "scheme",
428-
Host: "!$&'()*+,;=hello!:port",
428+
Host: "!$&'()*+,;=hello!:1",
429429
Path: "/path",
430430
},
431431
"",
@@ -1420,11 +1420,13 @@ func TestParseErrors(t *testing.T) {
14201420
{"http://[::1]", false},
14211421
{"http://[::1]:80", false},
14221422
{"http://[::1]:namedport", true}, // rfc3986 3.2.3
1423+
{"http://x:namedport", true}, // rfc3986 3.2.3
14231424
{"http://[::1]/", false},
14241425
{"http://[::1]a", true},
14251426
{"http://[::1]%23", true},
14261427
{"http://[::1%25en0]", false}, // valid zone id
14271428
{"http://[::1]:", false}, // colon, but no port OK
1429+
{"http://x:", false}, // colon, but no port OK
14281430
{"http://[::1]:%38%30", true}, // not allowed: % encoding only for non-ASCII
14291431
{"http://[::1%25%41]", false}, // RFC 6874 allows over-escaping in zone
14301432
{"http://[%10::1]", true}, // no %xx escapes in IP address
@@ -1616,46 +1618,46 @@ func TestURLErrorImplementsNetError(t *testing.T) {
16161618
}
16171619
}
16181620

1619-
func TestURLHostname(t *testing.T) {
1621+
func TestURLHostnameAndPort(t *testing.T) {
16201622
tests := []struct {
1621-
host string // URL.Host field
1622-
want string
1623+
in string // URL.Host field
1624+
host string
1625+
port string
16231626
}{
1624-
{"foo.com:80", "foo.com"},
1625-
{"foo.com", "foo.com"},
1626-
{"FOO.COM", "FOO.COM"}, // no canonicalization (yet?)
1627-
{"1.2.3.4", "1.2.3.4"},
1628-
{"1.2.3.4:80", "1.2.3.4"},
1629-
{"[1:2:3:4]", "1:2:3:4"},
1630-
{"[1:2:3:4]:80", "1:2:3:4"},
1631-
{"[::1]:80", "::1"},
1627+
{"foo.com:80", "foo.com", "80"},
1628+
{"foo.com", "foo.com", ""},
1629+
{"foo.com:", "foo.com", ""},
1630+
{"FOO.COM", "FOO.COM", ""}, // no canonicalization
1631+
{"1.2.3.4", "1.2.3.4", ""},
1632+
{"1.2.3.4:80", "1.2.3.4", "80"},
1633+
{"[1:2:3:4]", "1:2:3:4", ""},
1634+
{"[1:2:3:4]:80", "1:2:3:4", "80"},
1635+
{"[::1]:80", "::1", "80"},
1636+
{"[::1]", "::1", ""},
1637+
{"[::1]:", "::1", ""},
1638+
{"localhost", "localhost", ""},
1639+
{"localhost:443", "localhost", "443"},
1640+
{"some.super.long.domain.example.org:8080", "some.super.long.domain.example.org", "8080"},
1641+
{"[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:17000", "2001:0db8:85a3:0000:0000:8a2e:0370:7334", "17000"},
1642+
{"[2001:0db8:85a3:0000:0000:8a2e:0370:7334]", "2001:0db8:85a3:0000:0000:8a2e:0370:7334", ""},
1643+
1644+
// Ensure that even when not valid, Host is one of "Hostname",
1645+
// "Hostname:Port", "[Hostname]" or "[Hostname]:Port".
1646+
// See https://golang.org/issue/29098.
1647+
{"[google.com]:80", "google.com", "80"},
1648+
{"google.com]:80", "google.com]", "80"},
1649+
{"google.com:80_invalid_port", "google.com:80_invalid_port", ""},
1650+
{"[::1]extra]:80", "::1]extra", "80"},
1651+
{"google.com]extra:extra", "google.com]extra:extra", ""},
16321652
}
16331653
for _, tt := range tests {
1634-
u := &URL{Host: tt.host}
1635-
got := u.Hostname()
1636-
if got != tt.want {
1637-
t.Errorf("Hostname for Host %q = %q; want %q", tt.host, got, tt.want)
1654+
u := &URL{Host: tt.in}
1655+
host, port := u.Hostname(), u.Port()
1656+
if host != tt.host {
1657+
t.Errorf("Hostname for Host %q = %q; want %q", tt.in, host, tt.host)
16381658
}
1639-
}
1640-
}
1641-
1642-
func TestURLPort(t *testing.T) {
1643-
tests := []struct {
1644-
host string // URL.Host field
1645-
want string
1646-
}{
1647-
{"foo.com", ""},
1648-
{"foo.com:80", "80"},
1649-
{"1.2.3.4", ""},
1650-
{"1.2.3.4:80", "80"},
1651-
{"[1:2:3:4]", ""},
1652-
{"[1:2:3:4]:80", "80"},
1653-
}
1654-
for _, tt := range tests {
1655-
u := &URL{Host: tt.host}
1656-
got := u.Port()
1657-
if got != tt.want {
1658-
t.Errorf("Port for Host %q = %q; want %q", tt.host, got, tt.want)
1659+
if port != tt.port {
1660+
t.Errorf("Port for Host %q = %q; want %q", tt.in, port, tt.port)
16591661
}
16601662
}
16611663
}

0 commit comments

Comments
 (0)