]> Cypherpunks repositories - gostls13.git/commitdiff
net/http: add httpcookiemaxnum GODEBUG option to limit number of cookies parsed
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:23:20 +0000 (11:23 -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 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-review.googlesource.com/c/go/+/709855
Reviewed-by: Carlos Amedee <carlos@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
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 efe6cc3e77e5b5776569cabdf1e118714eab884c..f74bc1043c509e2dc67073fe4b785e75a321173e 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 8db4957b2cc37daae08e9a7063da673a2a8e0720..f452b4ec76830f9b994a668f1d0a09917903e863 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))
                        }
                }
        }
@@ -690,6 +735,7 @@ func TestParseCookie(t *testing.T) {
                line    string
                cookies []*Cookie
                err     error
+               godebug string
        }{
                {
                        line:    "Cookie-1=v$1",
@@ -723,8 +769,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 e40ce25ff9d1bc301355a5b23f4cdfb046c6607b..05646132ce4e69248bfd4fcf05271d6602cb99fb 100644 (file)
@@ -309,6 +309,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=...