]> Cypherpunks repositories - gostls13.git/commitdiff
[release-branch.go1.25] net/http: add httpcookiemaxnum GODEBUG option to limit number...
authorNicholas Husin <husin@google.com>
Tue, 30 Sep 2025 18:02:38 +0000 (14:02 -0400)
committerGopher Robot <gobot@golang.org>
Tue, 7 Oct 2025 18:02:18 +0000 (11:02 -0700)
When handling HTTP headers, net/http does not currently limit the number
of cookies that can be parsed. The only limitation that exists is for
the size of the entire HTTP header, which is controlled by
MaxHeaderBytes (defaults to 1 MB).

Unfortunately, this allows a malicious actor to send HTTP headers which
contain a massive amount of small cookies, such that as much cookies as
possible can be fitted within the MaxHeaderBytes limitation. Internally,
this causes us to allocate a massive number of Cookie struct.

For example, a 1 MB HTTP header with cookies that repeats "a=;" will
cause an allocation of ~66 MB in the heap. This can serve as a way for
malicious actors to induce memory exhaustion.

To fix this, we will now limit the number of cookies we are willing to
parse to 3000 by default. This behavior can be changed by setting a new
GODEBUG option: GODEBUG=httpcookiemaxnum. httpcookiemaxnum can be set to
allow a higher or lower cookie limit. Setting it to 0 will also allow an
infinite number of cookies to be parsed.

Thanks to jub0bs for reporting this issue.

For #75672
Fixes #75707
Fixes CVE-2025-58186

Change-Id: Ied58b3bc8acf5d11c880f881f36ecbf1d5d52622
Reviewed-on: https://go-internal-review.googlesource.com/c/go/+/2720
Reviewed-by: Roland Shoemaker <bracewell@google.com>
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-on: https://go-internal-review.googlesource.com/c/go/+/2965
Reviewed-by: Nicholas Husin <husin@google.com>
Commit-Queue: Roland Shoemaker <bracewell@google.com>
Reviewed-on: https://go-review.googlesource.com/c/go/+/709849
TryBot-Bypass: Michael Pratt <mpratt@google.com>
Reviewed-by: Carlos Amedee <carlos@golang.org>
Auto-Submit: Michael Pratt <mpratt@google.com>

doc/godebug.md
src/internal/godebugs/table.go
src/net/http/cookie.go
src/net/http/cookie_test.go
src/runtime/metrics/doc.go

index aaa0f9dd55e5707b20b49117437f6741938ed15a..c12ce5311d90d11bed1794f7b3f696fa6254270e 100644 (file)
@@ -153,6 +153,16 @@ for example,
 see the [runtime documentation](/pkg/runtime#hdr-Environment_Variables)
 and the [go command documentation](/cmd/go#hdr-Build_and_test_caching).
 
+### Go 1.26
+
+Go 1.26 added a new `httpcookiemaxnum` setting that controls the maximum number
+of cookies that net/http will accept when parsing HTTP headers. If the number of
+cookie in a header exceeds the number set in `httpcookiemaxnum`, cookie parsing
+will fail early. The default value is `httpcookiemaxnum=3000`. Setting
+`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.25
 
 Go 1.25 added a new `decoratemappings` setting that controls whether the Go
index 2d008825459bb27f2499a22c1f7b50d4fa44c663..852305e8553aab4069d58efc250ad743604e15f9 100644 (file)
@@ -42,6 +42,7 @@ var All = []Info{
        {Name: "http2client", Package: "net/http"},
        {Name: "http2debug", Package: "net/http", Opaque: true},
        {Name: "http2server", Package: "net/http"},
+       {Name: "httpcookiemaxnum", Package: "net/http", Changed: 24, Old: "0"},
        {Name: "httplaxcontentlength", Package: "net/http", Changed: 22, Old: "1"},
        {Name: "httpmuxgo121", Package: "net/http", Changed: 22, Old: "1"},
        {Name: "httpservecontentkeepheaders", Package: "net/http", Changed: 23, Old: "1"},
index 408fe88452b37afb097bbf1fc3e9ddf5956f5b16..d3c5c168ef21f1c9e3923a17049cb9a89bdd0fa5 100644 (file)
@@ -7,6 +7,7 @@ package http
 import (
        "errors"
        "fmt"
+       "internal/godebug"
        "log"
        "net"
        "net/http/internal/ascii"
@@ -16,6 +17,8 @@ import (
        "time"
 )
 
+var httpcookiemaxnum = godebug.New("httpcookiemaxnum")
+
 // A Cookie represents an HTTP cookie as sent in the Set-Cookie header of an
 // HTTP response or the Cookie header of an HTTP request.
 //
@@ -58,16 +61,37 @@ const (
 )
 
 var (
-       errBlankCookie           = errors.New("http: blank cookie")
-       errEqualNotFoundInCookie = errors.New("http: '=' not found in cookie")
-       errInvalidCookieName     = errors.New("http: invalid cookie name")
-       errInvalidCookieValue    = errors.New("http: invalid cookie value")
+       errBlankCookie            = errors.New("http: blank cookie")
+       errEqualNotFoundInCookie  = errors.New("http: '=' not found in cookie")
+       errInvalidCookieName      = errors.New("http: invalid cookie name")
+       errInvalidCookieValue     = errors.New("http: invalid cookie value")
+       errCookieNumLimitExceeded = errors.New("http: number of cookies exceeded limit")
 )
 
+const defaultCookieMaxNum = 3000
+
+func cookieNumWithinMax(cookieNum int) bool {
+       withinDefaultMax := cookieNum <= defaultCookieMaxNum
+       if httpcookiemaxnum.Value() == "" {
+               return withinDefaultMax
+       }
+       if customMax, err := strconv.Atoi(httpcookiemaxnum.Value()); err == nil {
+               withinCustomMax := customMax == 0 || cookieNum <= customMax
+               if withinDefaultMax != withinCustomMax {
+                       httpcookiemaxnum.IncNonDefault()
+               }
+               return withinCustomMax
+       }
+       return withinDefaultMax
+}
+
 // ParseCookie parses a Cookie header value and returns all the cookies
 // which were set in it. Since the same cookie name can appear multiple times
 // the returned Values can contain more than one value for a given key.
 func ParseCookie(line string) ([]*Cookie, error) {
+       if !cookieNumWithinMax(strings.Count(line, ";") + 1) {
+               return nil, errCookieNumLimitExceeded
+       }
        parts := strings.Split(textproto.TrimString(line), ";")
        if len(parts) == 1 && parts[0] == "" {
                return nil, errBlankCookie
@@ -197,11 +221,21 @@ func ParseSetCookie(line string) (*Cookie, error) {
 
 // readSetCookies parses all "Set-Cookie" values from
 // the header h and returns the successfully parsed Cookies.
+//
+// If the amount of cookies exceeds CookieNumLimit, and httpcookielimitnum
+// GODEBUG option is not explicitly turned off, this function will silently
+// fail and return an empty slice.
 func readSetCookies(h Header) []*Cookie {
        cookieCount := len(h["Set-Cookie"])
        if cookieCount == 0 {
                return []*Cookie{}
        }
+       // Cookie limit was unfortunately introduced at a later point in time.
+       // As such, we can only fail by returning an empty slice rather than
+       // explicit error.
+       if !cookieNumWithinMax(cookieCount) {
+               return []*Cookie{}
+       }
        cookies := make([]*Cookie, 0, cookieCount)
        for _, line := range h["Set-Cookie"] {
                if cookie, err := ParseSetCookie(line); err == nil {
@@ -329,13 +363,28 @@ func (c *Cookie) Valid() error {
 // readCookies parses all "Cookie" values from the header h and
 // returns the successfully parsed Cookies.
 //
-// if filter isn't empty, only cookies of that name are returned.
+// If filter isn't empty, only cookies of that name are returned.
+//
+// If the amount of cookies exceeds CookieNumLimit, and httpcookielimitnum
+// GODEBUG option is not explicitly turned off, this function will silently
+// fail and return an empty slice.
 func readCookies(h Header, filter string) []*Cookie {
        lines := h["Cookie"]
        if len(lines) == 0 {
                return []*Cookie{}
        }
 
+       // Cookie limit was unfortunately introduced at a later point in time.
+       // As such, we can only fail by returning an empty slice rather than
+       // explicit error.
+       cookieCount := 0
+       for _, line := range lines {
+               cookieCount += strings.Count(line, ";") + 1
+       }
+       if !cookieNumWithinMax(cookieCount) {
+               return []*Cookie{}
+       }
+
        cookies := make([]*Cookie, 0, len(lines)+strings.Count(lines[0], ";"))
        for _, line := range lines {
                line = textproto.TrimString(line)
index aac69563624fcde46e66bfd5e7816b8cc67b1736..d028725f31c4a3770a6178ccfa385d8256076582 100644 (file)
@@ -11,6 +11,7 @@ import (
        "log"
        "os"
        "reflect"
+       "slices"
        "strings"
        "testing"
        "time"
@@ -255,16 +256,17 @@ func TestAddCookie(t *testing.T) {
 }
 
 var readSetCookiesTests = []struct {
-       Header  Header
-       Cookies []*Cookie
+       header  Header
+       cookies []*Cookie
+       godebug string
 }{
        {
-               Header{"Set-Cookie": {"Cookie-1=v$1"}},
-               []*Cookie{{Name: "Cookie-1", Value: "v$1", Raw: "Cookie-1=v$1"}},
+               header:  Header{"Set-Cookie": {"Cookie-1=v$1"}},
+               cookies: []*Cookie{{Name: "Cookie-1", Value: "v$1", Raw: "Cookie-1=v$1"}},
        },
        {
-               Header{"Set-Cookie": {"NID=99=YsDT5i3E-CXax-; expires=Wed, 23-Nov-2011 01:05:03 GMT; path=/; domain=.google.ch; HttpOnly"}},
-               []*Cookie{{
+               header: Header{"Set-Cookie": {"NID=99=YsDT5i3E-CXax-; expires=Wed, 23-Nov-2011 01:05:03 GMT; path=/; domain=.google.ch; HttpOnly"}},
+               cookies: []*Cookie{{
                        Name:       "NID",
                        Value:      "99=YsDT5i3E-CXax-",
                        Path:       "/",
@@ -276,8 +278,8 @@ var readSetCookiesTests = []struct {
                }},
        },
        {
-               Header{"Set-Cookie": {".ASPXAUTH=7E3AA; expires=Wed, 07-Mar-2012 14:25:06 GMT; path=/; HttpOnly"}},
-               []*Cookie{{
+               header: Header{"Set-Cookie": {".ASPXAUTH=7E3AA; expires=Wed, 07-Mar-2012 14:25:06 GMT; path=/; HttpOnly"}},
+               cookies: []*Cookie{{
                        Name:       ".ASPXAUTH",
                        Value:      "7E3AA",
                        Path:       "/",
@@ -288,8 +290,8 @@ var readSetCookiesTests = []struct {
                }},
        },
        {
-               Header{"Set-Cookie": {"ASP.NET_SessionId=foo; path=/; HttpOnly"}},
-               []*Cookie{{
+               header: Header{"Set-Cookie": {"ASP.NET_SessionId=foo; path=/; HttpOnly"}},
+               cookies: []*Cookie{{
                        Name:     "ASP.NET_SessionId",
                        Value:    "foo",
                        Path:     "/",
@@ -298,8 +300,8 @@ var readSetCookiesTests = []struct {
                }},
        },
        {
-               Header{"Set-Cookie": {"samesitedefault=foo; SameSite"}},
-               []*Cookie{{
+               header: Header{"Set-Cookie": {"samesitedefault=foo; SameSite"}},
+               cookies: []*Cookie{{
                        Name:     "samesitedefault",
                        Value:    "foo",
                        SameSite: SameSiteDefaultMode,
@@ -307,8 +309,8 @@ var readSetCookiesTests = []struct {
                }},
        },
        {
-               Header{"Set-Cookie": {"samesiteinvalidisdefault=foo; SameSite=invalid"}},
-               []*Cookie{{
+               header: Header{"Set-Cookie": {"samesiteinvalidisdefault=foo; SameSite=invalid"}},
+               cookies: []*Cookie{{
                        Name:     "samesiteinvalidisdefault",
                        Value:    "foo",
                        SameSite: SameSiteDefaultMode,
@@ -316,8 +318,8 @@ var readSetCookiesTests = []struct {
                }},
        },
        {
-               Header{"Set-Cookie": {"samesitelax=foo; SameSite=Lax"}},
-               []*Cookie{{
+               header: Header{"Set-Cookie": {"samesitelax=foo; SameSite=Lax"}},
+               cookies: []*Cookie{{
                        Name:     "samesitelax",
                        Value:    "foo",
                        SameSite: SameSiteLaxMode,
@@ -325,8 +327,8 @@ var readSetCookiesTests = []struct {
                }},
        },
        {
-               Header{"Set-Cookie": {"samesitestrict=foo; SameSite=Strict"}},
-               []*Cookie{{
+               header: Header{"Set-Cookie": {"samesitestrict=foo; SameSite=Strict"}},
+               cookies: []*Cookie{{
                        Name:     "samesitestrict",
                        Value:    "foo",
                        SameSite: SameSiteStrictMode,
@@ -334,8 +336,8 @@ var readSetCookiesTests = []struct {
                }},
        },
        {
-               Header{"Set-Cookie": {"samesitenone=foo; SameSite=None"}},
-               []*Cookie{{
+               header: Header{"Set-Cookie": {"samesitenone=foo; SameSite=None"}},
+               cookies: []*Cookie{{
                        Name:     "samesitenone",
                        Value:    "foo",
                        SameSite: SameSiteNoneMode,
@@ -345,47 +347,66 @@ var readSetCookiesTests = []struct {
        // Make sure we can properly read back the Set-Cookie headers we create
        // for values containing spaces or commas:
        {
-               Header{"Set-Cookie": {`special-1=a z`}},
-               []*Cookie{{Name: "special-1", Value: "a z", Raw: `special-1=a z`}},
+               header:  Header{"Set-Cookie": {`special-1=a z`}},
+               cookies: []*Cookie{{Name: "special-1", Value: "a z", Raw: `special-1=a z`}},
        },
        {
-               Header{"Set-Cookie": {`special-2=" z"`}},
-               []*Cookie{{Name: "special-2", Value: " z", Quoted: true, Raw: `special-2=" z"`}},
+               header:  Header{"Set-Cookie": {`special-2=" z"`}},
+               cookies: []*Cookie{{Name: "special-2", Value: " z", Quoted: true, Raw: `special-2=" z"`}},
        },
        {
-               Header{"Set-Cookie": {`special-3="a "`}},
-               []*Cookie{{Name: "special-3", Value: "a ", Quoted: true, Raw: `special-3="a "`}},
+               header:  Header{"Set-Cookie": {`special-3="a "`}},
+               cookies: []*Cookie{{Name: "special-3", Value: "a ", Quoted: true, Raw: `special-3="a "`}},
        },
        {
-               Header{"Set-Cookie": {`special-4=" "`}},
-               []*Cookie{{Name: "special-4", Value: " ", Quoted: true, Raw: `special-4=" "`}},
+               header:  Header{"Set-Cookie": {`special-4=" "`}},
+               cookies: []*Cookie{{Name: "special-4", Value: " ", Quoted: true, Raw: `special-4=" "`}},
        },
        {
-               Header{"Set-Cookie": {`special-5=a,z`}},
-               []*Cookie{{Name: "special-5", Value: "a,z", Raw: `special-5=a,z`}},
+               header:  Header{"Set-Cookie": {`special-5=a,z`}},
+               cookies: []*Cookie{{Name: "special-5", Value: "a,z", Raw: `special-5=a,z`}},
        },
        {
-               Header{"Set-Cookie": {`special-6=",z"`}},
-               []*Cookie{{Name: "special-6", Value: ",z", Quoted: true, Raw: `special-6=",z"`}},
+               header:  Header{"Set-Cookie": {`special-6=",z"`}},
+               cookies: []*Cookie{{Name: "special-6", Value: ",z", Quoted: true, Raw: `special-6=",z"`}},
        },
        {
-               Header{"Set-Cookie": {`special-7=a,`}},
-               []*Cookie{{Name: "special-7", Value: "a,", Raw: `special-7=a,`}},
+               header:  Header{"Set-Cookie": {`special-7=a,`}},
+               cookies: []*Cookie{{Name: "special-7", Value: "a,", Raw: `special-7=a,`}},
        },
        {
-               Header{"Set-Cookie": {`special-8=","`}},
-               []*Cookie{{Name: "special-8", Value: ",", Quoted: true, Raw: `special-8=","`}},
+               header:  Header{"Set-Cookie": {`special-8=","`}},
+               cookies: []*Cookie{{Name: "special-8", Value: ",", Quoted: true, Raw: `special-8=","`}},
        },
        // Make sure we can properly read back the Set-Cookie headers
        // for names containing spaces:
        {
-               Header{"Set-Cookie": {`special-9 =","`}},
-               []*Cookie{{Name: "special-9", Value: ",", Quoted: true, Raw: `special-9 =","`}},
+               header:  Header{"Set-Cookie": {`special-9 =","`}},
+               cookies: []*Cookie{{Name: "special-9", Value: ",", Quoted: true, Raw: `special-9 =","`}},
        },
        // Quoted values (issue #46443)
        {
-               Header{"Set-Cookie": {`cookie="quoted"`}},
-               []*Cookie{{Name: "cookie", Value: "quoted", Quoted: true, Raw: `cookie="quoted"`}},
+               header:  Header{"Set-Cookie": {`cookie="quoted"`}},
+               cookies: []*Cookie{{Name: "cookie", Value: "quoted", Quoted: true, Raw: `cookie="quoted"`}},
+       },
+       {
+               header:  Header{"Set-Cookie": slices.Repeat([]string{"a="}, defaultCookieMaxNum+1)},
+               cookies: []*Cookie{},
+       },
+       {
+               header:  Header{"Set-Cookie": slices.Repeat([]string{"a="}, 10)},
+               cookies: []*Cookie{},
+               godebug: "httpcookiemaxnum=5",
+       },
+       {
+               header:  Header{"Set-Cookie": strings.Split(strings.Repeat(";a=", defaultCookieMaxNum+1)[1:], ";")},
+               cookies: slices.Repeat([]*Cookie{{Name: "a", Value: "", Quoted: false, Raw: "a="}}, defaultCookieMaxNum+1),
+               godebug: "httpcookiemaxnum=0",
+       },
+       {
+               header:  Header{"Set-Cookie": strings.Split(strings.Repeat(";a=", defaultCookieMaxNum+1)[1:], ";")},
+               cookies: slices.Repeat([]*Cookie{{Name: "a", Value: "", Quoted: false, Raw: "a="}}, defaultCookieMaxNum+1),
+               godebug: fmt.Sprintf("httpcookiemaxnum=%v", defaultCookieMaxNum+1),
        },
 
        // TODO(bradfitz): users have reported seeing this in the
@@ -405,79 +426,103 @@ func toJSON(v any) string {
 
 func TestReadSetCookies(t *testing.T) {
        for i, tt := range readSetCookiesTests {
+               t.Setenv("GODEBUG", tt.godebug)
                for n := 0; n < 2; n++ { // to verify readSetCookies doesn't mutate its input
-                       c := readSetCookies(tt.Header)
-                       if !reflect.DeepEqual(c, tt.Cookies) {
-                               t.Errorf("#%d readSetCookies: have\n%s\nwant\n%s\n", i, toJSON(c), toJSON(tt.Cookies))
+                       c := readSetCookies(tt.header)
+                       if !reflect.DeepEqual(c, tt.cookies) {
+                               t.Errorf("#%d readSetCookies: have\n%s\nwant\n%s\n", i, toJSON(c), toJSON(tt.cookies))
                        }
                }
        }
 }
 
 var readCookiesTests = []struct {
-       Header  Header
-       Filter  string
-       Cookies []*Cookie
+       header  Header
+       filter  string
+       cookies []*Cookie
+       godebug string
 }{
        {
-               Header{"Cookie": {"Cookie-1=v$1", "c2=v2"}},
-               "",
-               []*Cookie{
+               header: Header{"Cookie": {"Cookie-1=v$1", "c2=v2"}},
+               filter: "",
+               cookies: []*Cookie{
                        {Name: "Cookie-1", Value: "v$1"},
                        {Name: "c2", Value: "v2"},
                },
        },
        {
-               Header{"Cookie": {"Cookie-1=v$1", "c2=v2"}},
-               "c2",
-               []*Cookie{
+               header: Header{"Cookie": {"Cookie-1=v$1", "c2=v2"}},
+               filter: "c2",
+               cookies: []*Cookie{
                        {Name: "c2", Value: "v2"},
                },
        },
        {
-               Header{"Cookie": {"Cookie-1=v$1; c2=v2"}},
-               "",
-               []*Cookie{
+               header: Header{"Cookie": {"Cookie-1=v$1; c2=v2"}},
+               filter: "",
+               cookies: []*Cookie{
                        {Name: "Cookie-1", Value: "v$1"},
                        {Name: "c2", Value: "v2"},
                },
        },
        {
-               Header{"Cookie": {"Cookie-1=v$1; c2=v2"}},
-               "c2",
-               []*Cookie{
+               header: Header{"Cookie": {"Cookie-1=v$1; c2=v2"}},
+               filter: "c2",
+               cookies: []*Cookie{
                        {Name: "c2", Value: "v2"},
                },
        },
        {
-               Header{"Cookie": {`Cookie-1="v$1"; c2="v2"`}},
-               "",
-               []*Cookie{
+               header: Header{"Cookie": {`Cookie-1="v$1"; c2="v2"`}},
+               filter: "",
+               cookies: []*Cookie{
                        {Name: "Cookie-1", Value: "v$1", Quoted: true},
                        {Name: "c2", Value: "v2", Quoted: true},
                },
        },
        {
-               Header{"Cookie": {`Cookie-1="v$1"; c2=v2;`}},
-               "",
-               []*Cookie{
+               header: Header{"Cookie": {`Cookie-1="v$1"; c2=v2;`}},
+               filter: "",
+               cookies: []*Cookie{
                        {Name: "Cookie-1", Value: "v$1", Quoted: true},
                        {Name: "c2", Value: "v2"},
                },
        },
        {
-               Header{"Cookie": {``}},
-               "",
-               []*Cookie{},
+               header:  Header{"Cookie": {``}},
+               filter:  "",
+               cookies: []*Cookie{},
+       },
+       // GODEBUG=httpcookiemaxnum should work regardless if all cookies are sent
+       // via one "Cookie" field, or multiple fields.
+       {
+               header:  Header{"Cookie": {strings.Repeat(";a=", defaultCookieMaxNum+1)[1:]}},
+               cookies: []*Cookie{},
+       },
+       {
+               header:  Header{"Cookie": slices.Repeat([]string{"a="}, 10)},
+               cookies: []*Cookie{},
+               godebug: "httpcookiemaxnum=5",
+       },
+       {
+               header:  Header{"Cookie": {strings.Repeat(";a=", defaultCookieMaxNum+1)[1:]}},
+               cookies: slices.Repeat([]*Cookie{{Name: "a", Value: "", Quoted: false}}, defaultCookieMaxNum+1),
+               godebug: "httpcookiemaxnum=0",
+       },
+       {
+               header:  Header{"Cookie": slices.Repeat([]string{"a="}, defaultCookieMaxNum+1)},
+               cookies: slices.Repeat([]*Cookie{{Name: "a", Value: "", Quoted: false}}, defaultCookieMaxNum+1),
+               godebug: fmt.Sprintf("httpcookiemaxnum=%v", defaultCookieMaxNum+1),
        },
 }
 
 func TestReadCookies(t *testing.T) {
        for i, tt := range readCookiesTests {
+               t.Setenv("GODEBUG", tt.godebug)
                for n := 0; n < 2; n++ { // to verify readCookies doesn't mutate its input
-                       c := readCookies(tt.Header, tt.Filter)
-                       if !reflect.DeepEqual(c, tt.Cookies) {
-                               t.Errorf("#%d readCookies:\nhave: %s\nwant: %s\n", i, toJSON(c), toJSON(tt.Cookies))
+                       c := readCookies(tt.header, tt.filter)
+                       if !reflect.DeepEqual(c, tt.cookies) {
+                               t.Errorf("#%d readCookies:\nhave: %s\nwant: %s\n", i, toJSON(c), toJSON(tt.cookies))
                        }
                }
        }
@@ -689,6 +734,7 @@ func TestParseCookie(t *testing.T) {
                line    string
                cookies []*Cookie
                err     error
+               godebug string
        }{
                {
                        line:    "Cookie-1=v$1",
@@ -722,8 +768,28 @@ func TestParseCookie(t *testing.T) {
                        line: "k1=\\",
                        err:  errInvalidCookieValue,
                },
+               {
+                       line: strings.Repeat(";a=", defaultCookieMaxNum+1)[1:],
+                       err:  errCookieNumLimitExceeded,
+               },
+               {
+                       line:    strings.Repeat(";a=", 10)[1:],
+                       err:     errCookieNumLimitExceeded,
+                       godebug: "httpcookiemaxnum=5",
+               },
+               {
+                       line:    strings.Repeat(";a=", defaultCookieMaxNum+1)[1:],
+                       cookies: slices.Repeat([]*Cookie{{Name: "a", Value: "", Quoted: false}}, defaultCookieMaxNum+1),
+                       godebug: "httpcookiemaxnum=0",
+               },
+               {
+                       line:    strings.Repeat(";a=", defaultCookieMaxNum+1)[1:],
+                       cookies: slices.Repeat([]*Cookie{{Name: "a", Value: "", Quoted: false}}, defaultCookieMaxNum+1),
+                       godebug: fmt.Sprintf("httpcookiemaxnum=%v", defaultCookieMaxNum+1),
+               },
        }
        for i, tt := range tests {
+               t.Setenv("GODEBUG", tt.godebug)
                gotCookies, gotErr := ParseCookie(tt.line)
                if !errors.Is(gotErr, tt.err) {
                        t.Errorf("#%d ParseCookie got error %v, want error %v", i, gotErr, tt.err)
index a1902bc6d78c7f909abc9eef9b3cad4d46502208..251ee22fa1a51b9c95981873eec655042f578293 100644 (file)
@@ -282,6 +282,11 @@ Below is the full list of supported metrics, ordered lexicographically.
                The number of non-default behaviors executed by the net/http
                package due to a non-default GODEBUG=http2server=... setting.
 
+       /godebug/non-default-behavior/httpcookiemaxnum:events
+               The number of non-default behaviors executed by the net/http
+               package due to a non-default GODEBUG=httpcookiemaxnum=...
+               setting.
+
        /godebug/non-default-behavior/httplaxcontentlength:events
                The number of non-default behaviors executed by the net/http
                package due to a non-default GODEBUG=httplaxcontentlength=...