doc/godebug.md | 7 +++++++ src/internal/godebugs/table.go | 1 + src/net/url/url.go | 24 ++++++++++++++++++++++++ src/net/url/url_test.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ src/runtime/metrics/doc.go | 5 +++++ diff --git a/doc/godebug.md b/doc/godebug.md index c12ce5311d90d11bed1794f7b3f696fa6254270e..a6e64d16efe45de4e5d11e66486a425ada3d378b 100644 --- a/doc/godebug.md +++ b/doc/godebug.md @@ -163,6 +163,13 @@ `httpcookiemaxnum=0` will allow the cookie parsing to accept an indefinite 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 `urlmaxqueryparams` setting that controls the maximum number +of query parameters that net/url will accept when parsing a URL-encoded query string. +If the number of parameters exceeds the number set in `urlmaxqueryparams`, +parsing will fail early. The default value is `urlmaxqueryparams=10000`. +Setting `urlmaxqueryparams=0`bles the limit. To avoid denial of service attacks, +this setting and default was backported to Go 1.25.4 and Go 1.24.10. + ### Go 1.25 Go 1.25 added a new `decoratemappings` setting that controls whether the Go diff --git a/src/internal/godebugs/table.go b/src/internal/godebugs/table.go index 852305e8553aab4069d58efc250ad743604e15f9..1d4a329fef3d55bbef363117edbd5af717ba00e2 100644 --- a/src/internal/godebugs/table.go +++ b/src/internal/godebugs/table.go @@ -67,6 +67,7 @@ {Name: "tlsrsakex", Package: "crypto/tls", Changed: 22, Old: "1"}, {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: "urlmaxqueryparams", Package: "net/url", Changed: 24, 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"}, diff --git a/src/net/url/url.go b/src/net/url/url.go index 1c50e069613a812adffbe9cc97f20bdecf3d9cf8..a0974b1ed1be074ce713142140a7dd0f03abd2eb 100644 --- a/src/net/url/url.go +++ b/src/net/url/url.go @@ -15,6 +15,7 @@ import ( "errors" "fmt" + "internal/godebug" "maps" "net/netip" "path" @@ -992,7 +993,30 @@ err := parseQuery(m, query) return m, err } +var urlmaxqueryparams = godebug.New("urlmaxqueryparams") + +const defaultMaxParams = 10000 + +func urlParamsWithinMax(params int) bool { + withinDefaultMax := params <= defaultMaxParams + if urlmaxqueryparams.Value() == "" { + return withinDefaultMax + } + customMax, err := strconv.Atoi(urlmaxqueryparams.Value()) + if err != nil { + return withinDefaultMax + } + withinCustomMax := customMax == 0 || params < customMax + if withinDefaultMax != withinCustomMax { + urlmaxqueryparams.IncNonDefault() + } + return withinCustomMax +} + func parseQuery(m Values, query string) (err error) { + if !urlParamsWithinMax(strings.Count(query, "&") + 1) { + return errors.New("number of URL query parameters exceeded limit") + } for query != "" { var key string key, query, _ = strings.Cut(query, "&") diff --git a/src/net/url/url_test.go b/src/net/url/url_test.go index 6084facacc0519b0caebd232b67d0f8edbf43898..944124d20edbf76ed8744cd766e404838590abf0 100644 --- a/src/net/url/url_test.go +++ b/src/net/url/url_test.go @@ -1496,6 +1496,54 @@ }) } } +func TestParseQueryLimits(t *testing.T) { + for _, test := range []struct { + params int + godebug string + wantErr bool + }{{ + params: 10, + wantErr: false, + }, { + params: defaultMaxParams, + wantErr: false, + }, { + params: defaultMaxParams + 1, + wantErr: true, + }, { + params: 10, + godebug: "urlmaxqueryparams=9", + wantErr: true, + }, { + params: defaultMaxParams + 1, + godebug: "urlmaxqueryparams=0", + wantErr: false, + }} { + t.Setenv("GODEBUG", test.godebug) + want := Values{} + var b strings.Builder + for i := range test.params { + if i > 0 { + b.WriteString("&") + } + p := fmt.Sprintf("p%v", i) + b.WriteString(p) + want[p] = []string{""} + } + query := b.String() + got, err := ParseQuery(query) + if gotErr, wantErr := err != nil, test.wantErr; gotErr != wantErr { + t.Errorf("GODEBUG=%v ParseQuery(%v params) = %v, want error: %v", test.godebug, test.params, err, wantErr) + } + if err != nil { + continue + } + if got, want := len(got), test.params; got != want { + t.Errorf("GODEBUG=%v ParseQuery(%v params): got %v params, want %v", test.godebug, test.params, got, want) + } + } +} + type RequestURITest struct { url *URL out string diff --git a/src/runtime/metrics/doc.go b/src/runtime/metrics/doc.go index 251ee22fa1a51b9c95981873eec655042f578293..bd90b5f894c8b3856c85f6e7b1aa1665b0153629 100644 --- a/src/runtime/metrics/doc.go +++ b/src/runtime/metrics/doc.go @@ -372,6 +372,11 @@ /godebug/non-default-behavior/updatemaxprocs:events The number of non-default behaviors executed by the runtime package due to a non-default GODEBUG=updatemaxprocs=... setting. + /godebug/non-default-behavior/urlmaxqueryparams:events + The number of non-default behaviors executed by the net/url + package due to a non-default GODEBUG=urlmaxqueryparams=... + 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.