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 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. 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
Go 1.25 added a new `decoratemappings` setting that controls whether the Go 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: "tlssha1", Package: "crypto/tls", Changed: 25, Old: "1"},
{Name: "tlsunsafeekm", Package: "crypto/tls", Changed: 22, Old: "1"}, {Name: "tlsunsafeekm", Package: "crypto/tls", Changed: 22, Old: "1"},
{Name: "updatemaxprocs", Package: "runtime", Changed: 25, Old: "0"}, {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: "winreadlinkvolume", Package: "os", Changed: 23, Old: "0"},
{Name: "winsymlink", Package: "os", Changed: 23, Old: "0"}, {Name: "winsymlink", Package: "os", Changed: 23, Old: "0"},
{Name: "x509keypairleaf", Package: "crypto/tls", Changed: 23, Old: "0"}, {Name: "x509keypairleaf", Package: "crypto/tls", Changed: 23, Old: "0"},

View file

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

View file

@ -13,6 +13,7 @@ import (
"io" "io"
"net" "net"
"reflect" "reflect"
"strconv"
"strings" "strings"
"testing" "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", "http://[2b01:e34:ef40:7730:8e70:5aff:fefe:edac]:8080/foo",
&URL{ &URL{
@ -735,6 +716,9 @@ var parseRequestURLTests = []struct {
{"https://[0:0::test.com]:80", false}, {"https://[0:0::test.com]:80", false},
{"https://[2001:db8::test.com]", false}, {"https://[2001:db8::test.com]", false},
{"https://[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) { 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 The number of non-default behaviors executed by the runtime
package due to a non-default GODEBUG=updatemaxprocs=... setting. 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 /godebug/non-default-behavior/winreadlinkvolume:events
The number of non-default behaviors executed by the os package The number of non-default behaviors executed by the os package
due to a non-default GODEBUG=winreadlinkvolume=... setting. due to a non-default GODEBUG=winreadlinkvolume=... setting.