[release-branch.go1.25] net/http: add httpcookiemaxnum GODEBUG option to limit number of cookies parsed

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>
This commit is contained in:
Nicholas Husin 2025-09-30 14:02:38 -04:00 committed by Gopher Robot
parent f0c69db15a
commit 100c5a6680
5 changed files with 206 additions and 75 deletions

View file

@ -153,6 +153,16 @@ for example,
see the [runtime documentation](/pkg/runtime#hdr-Environment_Variables) see the [runtime documentation](/pkg/runtime#hdr-Environment_Variables)
and the [go command documentation](/cmd/go#hdr-Build_and_test_caching). 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
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

@ -42,6 +42,7 @@ var All = []Info{
{Name: "http2client", Package: "net/http"}, {Name: "http2client", Package: "net/http"},
{Name: "http2debug", Package: "net/http", Opaque: true}, {Name: "http2debug", Package: "net/http", Opaque: true},
{Name: "http2server", Package: "net/http"}, {Name: "http2server", Package: "net/http"},
{Name: "httpcookiemaxnum", Package: "net/http", Changed: 24, Old: "0"},
{Name: "httplaxcontentlength", Package: "net/http", Changed: 22, Old: "1"}, {Name: "httplaxcontentlength", Package: "net/http", Changed: 22, Old: "1"},
{Name: "httpmuxgo121", 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"}, {Name: "httpservecontentkeepheaders", Package: "net/http", Changed: 23, Old: "1"},

View file

@ -7,6 +7,7 @@ package http
import ( import (
"errors" "errors"
"fmt" "fmt"
"internal/godebug"
"log" "log"
"net" "net"
"net/http/internal/ascii" "net/http/internal/ascii"
@ -16,6 +17,8 @@ import (
"time" "time"
) )
var httpcookiemaxnum = godebug.New("httpcookiemaxnum")
// A Cookie represents an HTTP cookie as sent in the Set-Cookie header of an // 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. // HTTP response or the Cookie header of an HTTP request.
// //
@ -62,12 +65,33 @@ var (
errEqualNotFoundInCookie = errors.New("http: '=' not found in cookie") errEqualNotFoundInCookie = errors.New("http: '=' not found in cookie")
errInvalidCookieName = errors.New("http: invalid cookie name") errInvalidCookieName = errors.New("http: invalid cookie name")
errInvalidCookieValue = errors.New("http: invalid cookie value") 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 // 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 // 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. // the returned Values can contain more than one value for a given key.
func ParseCookie(line string) ([]*Cookie, error) { func ParseCookie(line string) ([]*Cookie, error) {
if !cookieNumWithinMax(strings.Count(line, ";") + 1) {
return nil, errCookieNumLimitExceeded
}
parts := strings.Split(textproto.TrimString(line), ";") parts := strings.Split(textproto.TrimString(line), ";")
if len(parts) == 1 && parts[0] == "" { if len(parts) == 1 && parts[0] == "" {
return nil, errBlankCookie return nil, errBlankCookie
@ -197,11 +221,21 @@ func ParseSetCookie(line string) (*Cookie, error) {
// readSetCookies parses all "Set-Cookie" values from // readSetCookies parses all "Set-Cookie" values from
// the header h and returns the successfully parsed Cookies. // 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 { func readSetCookies(h Header) []*Cookie {
cookieCount := len(h["Set-Cookie"]) cookieCount := len(h["Set-Cookie"])
if cookieCount == 0 { if cookieCount == 0 {
return []*Cookie{} 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) cookies := make([]*Cookie, 0, cookieCount)
for _, line := range h["Set-Cookie"] { for _, line := range h["Set-Cookie"] {
if cookie, err := ParseSetCookie(line); err == nil { 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 // readCookies parses all "Cookie" values from the header h and
// returns the successfully parsed Cookies. // 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 { func readCookies(h Header, filter string) []*Cookie {
lines := h["Cookie"] lines := h["Cookie"]
if len(lines) == 0 { if len(lines) == 0 {
return []*Cookie{} 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], ";")) cookies := make([]*Cookie, 0, len(lines)+strings.Count(lines[0], ";"))
for _, line := range lines { for _, line := range lines {
line = textproto.TrimString(line) line = textproto.TrimString(line)

View file

@ -11,6 +11,7 @@ import (
"log" "log"
"os" "os"
"reflect" "reflect"
"slices"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -255,16 +256,17 @@ func TestAddCookie(t *testing.T) {
} }
var readSetCookiesTests = []struct { var readSetCookiesTests = []struct {
Header Header header Header
Cookies []*Cookie cookies []*Cookie
godebug string
}{ }{
{ {
Header{"Set-Cookie": {"Cookie-1=v$1"}}, header: Header{"Set-Cookie": {"Cookie-1=v$1"}},
[]*Cookie{{Name: "Cookie-1", Value: "v$1", Raw: "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"}}, header: Header{"Set-Cookie": {"NID=99=YsDT5i3E-CXax-; expires=Wed, 23-Nov-2011 01:05:03 GMT; path=/; domain=.google.ch; HttpOnly"}},
[]*Cookie{{ cookies: []*Cookie{{
Name: "NID", Name: "NID",
Value: "99=YsDT5i3E-CXax-", Value: "99=YsDT5i3E-CXax-",
Path: "/", Path: "/",
@ -276,8 +278,8 @@ var readSetCookiesTests = []struct {
}}, }},
}, },
{ {
Header{"Set-Cookie": {".ASPXAUTH=7E3AA; expires=Wed, 07-Mar-2012 14:25:06 GMT; path=/; HttpOnly"}}, header: Header{"Set-Cookie": {".ASPXAUTH=7E3AA; expires=Wed, 07-Mar-2012 14:25:06 GMT; path=/; HttpOnly"}},
[]*Cookie{{ cookies: []*Cookie{{
Name: ".ASPXAUTH", Name: ".ASPXAUTH",
Value: "7E3AA", Value: "7E3AA",
Path: "/", Path: "/",
@ -288,8 +290,8 @@ var readSetCookiesTests = []struct {
}}, }},
}, },
{ {
Header{"Set-Cookie": {"ASP.NET_SessionId=foo; path=/; HttpOnly"}}, header: Header{"Set-Cookie": {"ASP.NET_SessionId=foo; path=/; HttpOnly"}},
[]*Cookie{{ cookies: []*Cookie{{
Name: "ASP.NET_SessionId", Name: "ASP.NET_SessionId",
Value: "foo", Value: "foo",
Path: "/", Path: "/",
@ -298,8 +300,8 @@ var readSetCookiesTests = []struct {
}}, }},
}, },
{ {
Header{"Set-Cookie": {"samesitedefault=foo; SameSite"}}, header: Header{"Set-Cookie": {"samesitedefault=foo; SameSite"}},
[]*Cookie{{ cookies: []*Cookie{{
Name: "samesitedefault", Name: "samesitedefault",
Value: "foo", Value: "foo",
SameSite: SameSiteDefaultMode, SameSite: SameSiteDefaultMode,
@ -307,8 +309,8 @@ var readSetCookiesTests = []struct {
}}, }},
}, },
{ {
Header{"Set-Cookie": {"samesiteinvalidisdefault=foo; SameSite=invalid"}}, header: Header{"Set-Cookie": {"samesiteinvalidisdefault=foo; SameSite=invalid"}},
[]*Cookie{{ cookies: []*Cookie{{
Name: "samesiteinvalidisdefault", Name: "samesiteinvalidisdefault",
Value: "foo", Value: "foo",
SameSite: SameSiteDefaultMode, SameSite: SameSiteDefaultMode,
@ -316,8 +318,8 @@ var readSetCookiesTests = []struct {
}}, }},
}, },
{ {
Header{"Set-Cookie": {"samesitelax=foo; SameSite=Lax"}}, header: Header{"Set-Cookie": {"samesitelax=foo; SameSite=Lax"}},
[]*Cookie{{ cookies: []*Cookie{{
Name: "samesitelax", Name: "samesitelax",
Value: "foo", Value: "foo",
SameSite: SameSiteLaxMode, SameSite: SameSiteLaxMode,
@ -325,8 +327,8 @@ var readSetCookiesTests = []struct {
}}, }},
}, },
{ {
Header{"Set-Cookie": {"samesitestrict=foo; SameSite=Strict"}}, header: Header{"Set-Cookie": {"samesitestrict=foo; SameSite=Strict"}},
[]*Cookie{{ cookies: []*Cookie{{
Name: "samesitestrict", Name: "samesitestrict",
Value: "foo", Value: "foo",
SameSite: SameSiteStrictMode, SameSite: SameSiteStrictMode,
@ -334,8 +336,8 @@ var readSetCookiesTests = []struct {
}}, }},
}, },
{ {
Header{"Set-Cookie": {"samesitenone=foo; SameSite=None"}}, header: Header{"Set-Cookie": {"samesitenone=foo; SameSite=None"}},
[]*Cookie{{ cookies: []*Cookie{{
Name: "samesitenone", Name: "samesitenone",
Value: "foo", Value: "foo",
SameSite: SameSiteNoneMode, SameSite: SameSiteNoneMode,
@ -345,47 +347,66 @@ var readSetCookiesTests = []struct {
// Make sure we can properly read back the Set-Cookie headers we create // Make sure we can properly read back the Set-Cookie headers we create
// for values containing spaces or commas: // for values containing spaces or commas:
{ {
Header{"Set-Cookie": {`special-1=a z`}}, header: Header{"Set-Cookie": {`special-1=a z`}},
[]*Cookie{{Name: "special-1", Value: "a z", Raw: `special-1=a z`}}, cookies: []*Cookie{{Name: "special-1", Value: "a z", Raw: `special-1=a z`}},
}, },
{ {
Header{"Set-Cookie": {`special-2=" z"`}}, header: Header{"Set-Cookie": {`special-2=" z"`}},
[]*Cookie{{Name: "special-2", Value: " z", Quoted: true, Raw: `special-2=" z"`}}, cookies: []*Cookie{{Name: "special-2", Value: " z", Quoted: true, Raw: `special-2=" z"`}},
}, },
{ {
Header{"Set-Cookie": {`special-3="a "`}}, header: Header{"Set-Cookie": {`special-3="a "`}},
[]*Cookie{{Name: "special-3", Value: "a ", Quoted: true, Raw: `special-3="a "`}}, cookies: []*Cookie{{Name: "special-3", Value: "a ", Quoted: true, Raw: `special-3="a "`}},
}, },
{ {
Header{"Set-Cookie": {`special-4=" "`}}, header: Header{"Set-Cookie": {`special-4=" "`}},
[]*Cookie{{Name: "special-4", Value: " ", Quoted: true, Raw: `special-4=" "`}}, cookies: []*Cookie{{Name: "special-4", Value: " ", Quoted: true, Raw: `special-4=" "`}},
}, },
{ {
Header{"Set-Cookie": {`special-5=a,z`}}, header: Header{"Set-Cookie": {`special-5=a,z`}},
[]*Cookie{{Name: "special-5", Value: "a,z", Raw: `special-5=a,z`}}, cookies: []*Cookie{{Name: "special-5", Value: "a,z", Raw: `special-5=a,z`}},
}, },
{ {
Header{"Set-Cookie": {`special-6=",z"`}}, header: Header{"Set-Cookie": {`special-6=",z"`}},
[]*Cookie{{Name: "special-6", Value: ",z", Quoted: true, Raw: `special-6=",z"`}}, cookies: []*Cookie{{Name: "special-6", Value: ",z", Quoted: true, Raw: `special-6=",z"`}},
}, },
{ {
Header{"Set-Cookie": {`special-7=a,`}}, header: Header{"Set-Cookie": {`special-7=a,`}},
[]*Cookie{{Name: "special-7", Value: "a,", Raw: `special-7=a,`}}, cookies: []*Cookie{{Name: "special-7", Value: "a,", Raw: `special-7=a,`}},
}, },
{ {
Header{"Set-Cookie": {`special-8=","`}}, header: Header{"Set-Cookie": {`special-8=","`}},
[]*Cookie{{Name: "special-8", Value: ",", Quoted: true, Raw: `special-8=","`}}, cookies: []*Cookie{{Name: "special-8", Value: ",", Quoted: true, Raw: `special-8=","`}},
}, },
// Make sure we can properly read back the Set-Cookie headers // Make sure we can properly read back the Set-Cookie headers
// for names containing spaces: // for names containing spaces:
{ {
Header{"Set-Cookie": {`special-9 =","`}}, header: Header{"Set-Cookie": {`special-9 =","`}},
[]*Cookie{{Name: "special-9", Value: ",", Quoted: true, Raw: `special-9 =","`}}, cookies: []*Cookie{{Name: "special-9", Value: ",", Quoted: true, Raw: `special-9 =","`}},
}, },
// Quoted values (issue #46443) // Quoted values (issue #46443)
{ {
Header{"Set-Cookie": {`cookie="quoted"`}}, header: Header{"Set-Cookie": {`cookie="quoted"`}},
[]*Cookie{{Name: "cookie", Value: "quoted", Quoted: true, Raw: `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 // TODO(bradfitz): users have reported seeing this in the
@ -405,79 +426,103 @@ func toJSON(v any) string {
func TestReadSetCookies(t *testing.T) { func TestReadSetCookies(t *testing.T) {
for i, tt := range readSetCookiesTests { for i, tt := range readSetCookiesTests {
t.Setenv("GODEBUG", tt.godebug)
for n := 0; n < 2; n++ { // to verify readSetCookies doesn't mutate its input for n := 0; n < 2; n++ { // to verify readSetCookies doesn't mutate its input
c := readSetCookies(tt.Header) c := readSetCookies(tt.header)
if !reflect.DeepEqual(c, tt.Cookies) { if !reflect.DeepEqual(c, tt.cookies) {
t.Errorf("#%d readSetCookies: have\n%s\nwant\n%s\n", i, toJSON(c), toJSON(tt.Cookies)) t.Errorf("#%d readSetCookies: have\n%s\nwant\n%s\n", i, toJSON(c), toJSON(tt.cookies))
} }
} }
} }
} }
var readCookiesTests = []struct { var readCookiesTests = []struct {
Header Header header Header
Filter string filter string
Cookies []*Cookie cookies []*Cookie
godebug string
}{ }{
{ {
Header{"Cookie": {"Cookie-1=v$1", "c2=v2"}}, header: Header{"Cookie": {"Cookie-1=v$1", "c2=v2"}},
"", filter: "",
[]*Cookie{ cookies: []*Cookie{
{Name: "Cookie-1", Value: "v$1"}, {Name: "Cookie-1", Value: "v$1"},
{Name: "c2", Value: "v2"}, {Name: "c2", Value: "v2"},
}, },
}, },
{ {
Header{"Cookie": {"Cookie-1=v$1", "c2=v2"}}, header: Header{"Cookie": {"Cookie-1=v$1", "c2=v2"}},
"c2", filter: "c2",
[]*Cookie{ cookies: []*Cookie{
{Name: "c2", Value: "v2"}, {Name: "c2", Value: "v2"},
}, },
}, },
{ {
Header{"Cookie": {"Cookie-1=v$1; c2=v2"}}, header: Header{"Cookie": {"Cookie-1=v$1; c2=v2"}},
"", filter: "",
[]*Cookie{ cookies: []*Cookie{
{Name: "Cookie-1", Value: "v$1"}, {Name: "Cookie-1", Value: "v$1"},
{Name: "c2", Value: "v2"}, {Name: "c2", Value: "v2"},
}, },
}, },
{ {
Header{"Cookie": {"Cookie-1=v$1; c2=v2"}}, header: Header{"Cookie": {"Cookie-1=v$1; c2=v2"}},
"c2", filter: "c2",
[]*Cookie{ cookies: []*Cookie{
{Name: "c2", Value: "v2"}, {Name: "c2", Value: "v2"},
}, },
}, },
{ {
Header{"Cookie": {`Cookie-1="v$1"; c2="v2"`}}, header: Header{"Cookie": {`Cookie-1="v$1"; c2="v2"`}},
"", filter: "",
[]*Cookie{ cookies: []*Cookie{
{Name: "Cookie-1", Value: "v$1", Quoted: true}, {Name: "Cookie-1", Value: "v$1", Quoted: true},
{Name: "c2", Value: "v2", Quoted: true}, {Name: "c2", Value: "v2", Quoted: true},
}, },
}, },
{ {
Header{"Cookie": {`Cookie-1="v$1"; c2=v2;`}}, header: Header{"Cookie": {`Cookie-1="v$1"; c2=v2;`}},
"", filter: "",
[]*Cookie{ cookies: []*Cookie{
{Name: "Cookie-1", Value: "v$1", Quoted: true}, {Name: "Cookie-1", Value: "v$1", Quoted: true},
{Name: "c2", Value: "v2"}, {Name: "c2", Value: "v2"},
}, },
}, },
{ {
Header{"Cookie": {``}}, header: Header{"Cookie": {``}},
"", filter: "",
[]*Cookie{}, 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) { func TestReadCookies(t *testing.T) {
for i, tt := range readCookiesTests { for i, tt := range readCookiesTests {
t.Setenv("GODEBUG", tt.godebug)
for n := 0; n < 2; n++ { // to verify readCookies doesn't mutate its input for n := 0; n < 2; n++ { // to verify readCookies doesn't mutate its input
c := readCookies(tt.Header, tt.Filter) c := readCookies(tt.header, tt.filter)
if !reflect.DeepEqual(c, tt.Cookies) { if !reflect.DeepEqual(c, tt.cookies) {
t.Errorf("#%d readCookies:\nhave: %s\nwant: %s\n", i, toJSON(c), toJSON(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 line string
cookies []*Cookie cookies []*Cookie
err error err error
godebug string
}{ }{
{ {
line: "Cookie-1=v$1", line: "Cookie-1=v$1",
@ -722,8 +768,28 @@ func TestParseCookie(t *testing.T) {
line: "k1=\\", line: "k1=\\",
err: errInvalidCookieValue, 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 { for i, tt := range tests {
t.Setenv("GODEBUG", tt.godebug)
gotCookies, gotErr := ParseCookie(tt.line) gotCookies, gotErr := ParseCookie(tt.line)
if !errors.Is(gotErr, tt.err) { if !errors.Is(gotErr, tt.err) {
t.Errorf("#%d ParseCookie got error %v, want error %v", i, gotErr, tt.err) t.Errorf("#%d ParseCookie got error %v, want error %v", i, gotErr, tt.err)

View 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 The number of non-default behaviors executed by the net/http
package due to a non-default GODEBUG=http2server=... setting. 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 /godebug/non-default-behavior/httplaxcontentlength:events
The number of non-default behaviors executed by the net/http The number of non-default behaviors executed by the net/http
package due to a non-default GODEBUG=httplaxcontentlength=... package due to a non-default GODEBUG=httplaxcontentlength=...