]> Cypherpunks repositories - gostls13.git/commitdiff
net/url: disallow raw IPv6 addresses in host
authorSean Liao <sean@liao.dev>
Sat, 18 Oct 2025 09:31:12 +0000 (10:31 +0100)
committerSean Liao <sean@liao.dev>
Wed, 12 Nov 2025 18:02:50 +0000 (10:02 -0800)
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>

doc/godebug.md
doc/next/6-stdlib/99-minor/net/url/31024.md [new file with mode: 0644]
src/internal/godebugs/table.go
src/net/url/url.go
src/net/url/url_test.go
src/runtime/metrics/doc.go

index c12ce5311d90d11bed1794f7b3f696fa6254270e..d9ae462b980d88bd223c3c0586cf103dc28557d9 100644 (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
diff --git a/doc/next/6-stdlib/99-minor/net/url/31024.md b/doc/next/6-stdlib/99-minor/net/url/31024.md
new file mode 100644 (file)
index 0000000..11ed31e
--- /dev/null
@@ -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.
index 271c58648dc2cd0395818dea0f25ba2cbfbe5952..4939e6ff10981d18517fceee238542c7db14a920 100644 (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"},
index 6d1d50534355536c2d82814027f635fcae940e74..ca5ff9e3d70f27fd91c06f4baf054554073218fb 100644 (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)
index 501558403ac771eb17efbc4ce4c5fa40f9e5802f..b601849ce5581d29da2b5f4bcdda10469187a22e 100644 (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)
+                       }
+               })
+       }
+
+}
index 05646132ce4e69248bfd4fcf05271d6602cb99fb..8f908f5b520f730bdc8d936ae1507b8642503320 100644 (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.