import (
"errors"
"fmt"
+ "internal/godebug"
"log"
"net"
"net/http/internal/ascii"
"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.
//
)
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
// 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 {
// 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)
"log"
"os"
"reflect"
+ "slices"
"strings"
"testing"
"time"
}
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: "/",
}},
},
{
- 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: "/",
}},
},
{
- 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: "/",
}},
},
{
- Header{"Set-Cookie": {"samesitedefault=foo; SameSite"}},
- []*Cookie{{
+ header: Header{"Set-Cookie": {"samesitedefault=foo; SameSite"}},
+ cookies: []*Cookie{{
Name: "samesitedefault",
Value: "foo",
SameSite: SameSiteDefaultMode,
}},
},
{
- Header{"Set-Cookie": {"samesiteinvalidisdefault=foo; SameSite=invalid"}},
- []*Cookie{{
+ header: Header{"Set-Cookie": {"samesiteinvalidisdefault=foo; SameSite=invalid"}},
+ cookies: []*Cookie{{
Name: "samesiteinvalidisdefault",
Value: "foo",
SameSite: SameSiteDefaultMode,
}},
},
{
- Header{"Set-Cookie": {"samesitelax=foo; SameSite=Lax"}},
- []*Cookie{{
+ header: Header{"Set-Cookie": {"samesitelax=foo; SameSite=Lax"}},
+ cookies: []*Cookie{{
Name: "samesitelax",
Value: "foo",
SameSite: SameSiteLaxMode,
}},
},
{
- Header{"Set-Cookie": {"samesitestrict=foo; SameSite=Strict"}},
- []*Cookie{{
+ header: Header{"Set-Cookie": {"samesitestrict=foo; SameSite=Strict"}},
+ cookies: []*Cookie{{
Name: "samesitestrict",
Value: "foo",
SameSite: SameSiteStrictMode,
}},
},
{
- Header{"Set-Cookie": {"samesitenone=foo; SameSite=None"}},
- []*Cookie{{
+ header: Header{"Set-Cookie": {"samesitenone=foo; SameSite=None"}},
+ cookies: []*Cookie{{
Name: "samesitenone",
Value: "foo",
SameSite: SameSiteNoneMode,
// 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
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))
}
}
}
line string
cookies []*Cookie
err error
+ godebug string
}{
{
line: "Cookie-1=v$1",
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)