net/url: disallow raw IPv6 addresses in host

RFC 3986 requires square brackets around IPv6 addresses.
Parse's acceptance of raw IPv6 addresses is non compliant,
and complicates splitting out a port.

This is a resubmission of CL 710176 after the revert in CL 711800,
this time with a new urlstrictipv6 godebug to control the behavior.

Fixes #31024
Fixes #75223

Change-Id: I4cbe5bb84266b3efe9c98cf4300421ddf1df7291
Reviewed-on: https://go-review.googlesource.com/c/go/+/712840
Reviewed-by: Junyang Shao <shaojunyang@google.com>
Reviewed-by: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
Sean Liao 2025-10-18 10:31:12 +01:00
parent 4e761b9a18
commit 0c28789bd7
6 changed files with 49 additions and 21 deletions

View file

@ -163,6 +163,11 @@ will fail early. The default value is `httpcookiemaxnum=3000`. Setting
number of cookies. To avoid denial of service attacks, this setting and default
was backported to Go 1.25.2 and Go 1.24.8.
Go 1.26 added a new `urlstrictcolons` setting that controls whether `net/url.Parse`
allows malformed hostnames containing colons outside of a bracketed IPv6 address.
The default `urlstrictcolons=1` rejects URLs such as `http://localhost:1:2` or `http://::1/`.
Colons are permitted as part of a bracketed IPv6 address, such as `http://[::1]/`.
### Go 1.25
Go 1.25 added a new `decoratemappings` setting that controls whether the Go

View file

@ -0,0 +1,4 @@
[Parse] now rejects malformed URLs containing colons in the host subcomponent,
such as `http://::1/` or `http://localhost:80:80/`.
URLs containing bracketed IPv6 addresses, such as `http://[::1]/` are still accepted.
The new GODEBUG=urlstrictcolons=0 setting restores the old behavior.

View file

@ -67,6 +67,7 @@ var All = []Info{
{Name: "tlssha1", Package: "crypto/tls", Changed: 25, Old: "1"},
{Name: "tlsunsafeekm", Package: "crypto/tls", Changed: 22, Old: "1"},
{Name: "updatemaxprocs", Package: "runtime", Changed: 25, Old: "0"},
{Name: "urlstrictcolons", Package: "net/url", Changed: 26, Old: "0"},
{Name: "winreadlinkvolume", Package: "os", Changed: 23, Old: "0"},
{Name: "winsymlink", Package: "os", Changed: 23, Old: "0"},
{Name: "x509keypairleaf", Package: "crypto/tls", Changed: 23, Old: "0"},

View file

@ -18,6 +18,7 @@ package url
import (
"errors"
"fmt"
"internal/godebug"
"net/netip"
"path"
"slices"
@ -26,6 +27,8 @@ import (
_ "unsafe" // for linkname
)
var urlstrictcolons = godebug.New("urlstrictcolons")
// Error reports an error and the operation and URL that caused it.
type Error struct {
Op string
@ -599,7 +602,11 @@ func parseHost(host string) (string, error) {
return "", errors.New("invalid IP-literal")
}
return "[" + unescapedHostname + "]" + unescapedColonPort, nil
} else if i := strings.LastIndex(host, ":"); i != -1 {
} else if i := strings.Index(host, ":"); i != -1 {
if j := strings.LastIndex(host, ":"); urlstrictcolons.Value() == "0" && j != i {
urlstrictcolons.IncNonDefault()
i = j
}
colonPort := host[i:]
if !validOptionalPort(colonPort) {
return "", fmt.Errorf("invalid port %q after host", colonPort)

View file

@ -13,6 +13,7 @@ import (
"io"
"net"
"reflect"
"strconv"
"strings"
"testing"
)
@ -506,26 +507,6 @@ var urltests = []URLTest{
},
"",
},
{
// Malformed IPv6 but still accepted.
"http://2b01:e34:ef40:7730:8e70:5aff:fefe:edac:8080/foo",
&URL{
Scheme: "http",
Host: "2b01:e34:ef40:7730:8e70:5aff:fefe:edac:8080",
Path: "/foo",
},
"",
},
{
// Malformed IPv6 but still accepted.
"http://2b01:e34:ef40:7730:8e70:5aff:fefe:edac:/foo",
&URL{
Scheme: "http",
Host: "2b01:e34:ef40:7730:8e70:5aff:fefe:edac:",
Path: "/foo",
},
"",
},
{
"http://[2b01:e34:ef40:7730:8e70:5aff:fefe:edac]:8080/foo",
&URL{
@ -735,6 +716,9 @@ var parseRequestURLTests = []struct {
{"https://[0:0::test.com]:80", false},
{"https://[2001:db8::test.com]", false},
{"https://[test.com]", false},
{"https://1:2:3:4:5:6:7:8", false},
{"https://1:2:3:4:5:6:7:8:80", false},
{"https://example.com:80:", false},
}
func TestParseRequestURI(t *testing.T) {
@ -2280,3 +2264,25 @@ func TestJoinPath(t *testing.T) {
}
}
}
func TestParseStrictIpv6(t *testing.T) {
t.Setenv("GODEBUG", "urlstrictcolons=0")
tests := []struct {
url string
}{
// Malformed URLs that used to parse.
{"https://1:2:3:4:5:6:7:8"},
{"https://1:2:3:4:5:6:7:8:80"},
{"https://example.com:80:"},
}
for i, tc := range tests {
t.Run(strconv.Itoa(i), func(t *testing.T) {
_, err := Parse(tc.url)
if err != nil {
t.Errorf("Parse(%q) error = %v, want nil", tc.url, err)
}
})
}
}

View file

@ -399,6 +399,11 @@ Below is the full list of supported metrics, ordered lexicographically.
The number of non-default behaviors executed by the runtime
package due to a non-default GODEBUG=updatemaxprocs=... setting.
/godebug/non-default-behavior/urlstrictcolons:events
The number of non-default behaviors executed by the net/url
package due to a non-default GODEBUG=urlstrictcolons=...
setting.
/godebug/non-default-behavior/winreadlinkvolume:events
The number of non-default behaviors executed by the os package
due to a non-default GODEBUG=winreadlinkvolume=... setting.