Skip to content

Commit 9b9d02c

Browse files
nicholashusingopherbot
authored andcommitted
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 golang#75672 Fixes CVE-2025-58186 Change-Id: Ied58b3bc8acf5d11c880f881f36ecbf1d5d52622 Reviewed-on: https://go-internal-review.googlesource.com/c/go/+/2720 Reviewed-by: Roland Shoemaker <[email protected]> Reviewed-by: Damien Neil <[email protected]> Reviewed-on: https://go-review.googlesource.com/c/go/+/709855 Reviewed-by: Carlos Amedee <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Auto-Submit: Michael Pratt <[email protected]>
1 parent 3fc4c79 commit 9b9d02c

File tree

5 files changed

+206
-75
lines changed

5 files changed

+206
-75
lines changed

doc/godebug.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,16 @@ for example,
153153
see the [runtime documentation](/pkg/runtime#hdr-Environment_Variables)
154154
and the [go command documentation](/cmd/go#hdr-Build_and_test_caching).
155155

156+
### Go 1.26
157+
158+
Go 1.26 added a new `httpcookiemaxnum` setting that controls the maximum number
159+
of cookies that net/http will accept when parsing HTTP headers. If the number of
160+
cookie in a header exceeds the number set in `httpcookiemaxnum`, cookie parsing
161+
will fail early. The default value is `httpcookiemaxnum=3000`. Setting
162+
`httpcookiemaxnum=0` will allow the cookie parsing to accept an indefinite
163+
number of cookies. To avoid denial of service attacks, this setting and default
164+
was backported to Go 1.25.2 and Go 1.24.8.
165+
156166
### Go 1.25
157167

158168
Go 1.25 added a new `decoratemappings` setting that controls whether the Go

src/internal/godebugs/table.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ var All = []Info{
4242
{Name: "http2client", Package: "net/http"},
4343
{Name: "http2debug", Package: "net/http", Opaque: true},
4444
{Name: "http2server", Package: "net/http"},
45+
{Name: "httpcookiemaxnum", Package: "net/http", Changed: 24, Old: "0"},
4546
{Name: "httplaxcontentlength", Package: "net/http", Changed: 22, Old: "1"},
4647
{Name: "httpmuxgo121", Package: "net/http", Changed: 22, Old: "1"},
4748
{Name: "httpservecontentkeepheaders", Package: "net/http", Changed: 23, Old: "1"},

src/net/http/cookie.go

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package http
77
import (
88
"errors"
99
"fmt"
10+
"internal/godebug"
1011
"log"
1112
"net"
1213
"net/http/internal/ascii"
@@ -16,6 +17,8 @@ import (
1617
"time"
1718
)
1819

20+
var httpcookiemaxnum = godebug.New("httpcookiemaxnum")
21+
1922
// A Cookie represents an HTTP cookie as sent in the Set-Cookie header of an
2023
// HTTP response or the Cookie header of an HTTP request.
2124
//
@@ -58,16 +61,37 @@ const (
5861
)
5962

6063
var (
61-
errBlankCookie = errors.New("http: blank cookie")
62-
errEqualNotFoundInCookie = errors.New("http: '=' not found in cookie")
63-
errInvalidCookieName = errors.New("http: invalid cookie name")
64-
errInvalidCookieValue = errors.New("http: invalid cookie value")
64+
errBlankCookie = errors.New("http: blank cookie")
65+
errEqualNotFoundInCookie = errors.New("http: '=' not found in cookie")
66+
errInvalidCookieName = errors.New("http: invalid cookie name")
67+
errInvalidCookieValue = errors.New("http: invalid cookie value")
68+
errCookieNumLimitExceeded = errors.New("http: number of cookies exceeded limit")
6569
)
6670

71+
const defaultCookieMaxNum = 3000
72+
73+
func cookieNumWithinMax(cookieNum int) bool {
74+
withinDefaultMax := cookieNum <= defaultCookieMaxNum
75+
if httpcookiemaxnum.Value() == "" {
76+
return withinDefaultMax
77+
}
78+
if customMax, err := strconv.Atoi(httpcookiemaxnum.Value()); err == nil {
79+
withinCustomMax := customMax == 0 || cookieNum <= customMax
80+
if withinDefaultMax != withinCustomMax {
81+
httpcookiemaxnum.IncNonDefault()
82+
}
83+
return withinCustomMax
84+
}
85+
return withinDefaultMax
86+
}
87+
6788
// ParseCookie parses a Cookie header value and returns all the cookies
6889
// which were set in it. Since the same cookie name can appear multiple times
6990
// the returned Values can contain more than one value for a given key.
7091
func ParseCookie(line string) ([]*Cookie, error) {
92+
if !cookieNumWithinMax(strings.Count(line, ";") + 1) {
93+
return nil, errCookieNumLimitExceeded
94+
}
7195
parts := strings.Split(textproto.TrimString(line), ";")
7296
if len(parts) == 1 && parts[0] == "" {
7397
return nil, errBlankCookie
@@ -197,11 +221,21 @@ func ParseSetCookie(line string) (*Cookie, error) {
197221

198222
// readSetCookies parses all "Set-Cookie" values from
199223
// the header h and returns the successfully parsed Cookies.
224+
//
225+
// If the amount of cookies exceeds CookieNumLimit, and httpcookielimitnum
226+
// GODEBUG option is not explicitly turned off, this function will silently
227+
// fail and return an empty slice.
200228
func readSetCookies(h Header) []*Cookie {
201229
cookieCount := len(h["Set-Cookie"])
202230
if cookieCount == 0 {
203231
return []*Cookie{}
204232
}
233+
// Cookie limit was unfortunately introduced at a later point in time.
234+
// As such, we can only fail by returning an empty slice rather than
235+
// explicit error.
236+
if !cookieNumWithinMax(cookieCount) {
237+
return []*Cookie{}
238+
}
205239
cookies := make([]*Cookie, 0, cookieCount)
206240
for _, line := range h["Set-Cookie"] {
207241
if cookie, err := ParseSetCookie(line); err == nil {
@@ -329,13 +363,28 @@ func (c *Cookie) Valid() error {
329363
// readCookies parses all "Cookie" values from the header h and
330364
// returns the successfully parsed Cookies.
331365
//
332-
// if filter isn't empty, only cookies of that name are returned.
366+
// If filter isn't empty, only cookies of that name are returned.
367+
//
368+
// If the amount of cookies exceeds CookieNumLimit, and httpcookielimitnum
369+
// GODEBUG option is not explicitly turned off, this function will silently
370+
// fail and return an empty slice.
333371
func readCookies(h Header, filter string) []*Cookie {
334372
lines := h["Cookie"]
335373
if len(lines) == 0 {
336374
return []*Cookie{}
337375
}
338376

377+
// Cookie limit was unfortunately introduced at a later point in time.
378+
// As such, we can only fail by returning an empty slice rather than
379+
// explicit error.
380+
cookieCount := 0
381+
for _, line := range lines {
382+
cookieCount += strings.Count(line, ";") + 1
383+
}
384+
if !cookieNumWithinMax(cookieCount) {
385+
return []*Cookie{}
386+
}
387+
339388
cookies := make([]*Cookie, 0, len(lines)+strings.Count(lines[0], ";"))
340389
for _, line := range lines {
341390
line = textproto.TrimString(line)

0 commit comments

Comments
 (0)