git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge pull request #656 from grnhse/cookie-splitting-precision

Split cookies more precisely at 4096 bytes

commit016f4aa276e0a34d608dedfb26c71f769a7bf458 authorNick Meves <nick.meves@greenhouse.io> dateSat 4 Jul 2020, 11:15 (6 years ago) parentc4cf15f48a2aaa
3 files changed+144−14
CHANGELOG.md+1 pkg/sessions/cookie/session_store.go+32−14 pkg/sessions/cookie/session_store_test.go+111
CHANGELOG.md+1
@@ -8,6 +8,7 @@
88
99## Changes since v6.0.0
1010
11+- [#656](https://github.com/oauth2-proxy/oauth2-proxy/pull/656) Split long session cookies more precisely (@NickMeves)
1112- [#619](https://github.com/oauth2-proxy/oauth2-proxy/pull/619) Improve Redirect to HTTPs behaviour (@JoelSpeed)
1213- [#654](https://github.com/oauth2-proxy/oauth2-proxy/pull/654) Close client connections after each redis test (@JoelSpeed)
1314- [#542](https://github.com/oauth2-proxy/oauth2-proxy/pull/542) Move SessionStore tests to independent package (@JoelSpeed)
pkg/sessions/cookie/session_store.go+32−14
@@ -10,15 +10,16 @@ import (
1010
1111 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
1212 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
13− "github.com/oauth2-proxy/oauth2-proxy/pkg/cookies"
13+ pkgcookies "github.com/oauth2-proxy/oauth2-proxy/pkg/cookies"
1414 "github.com/oauth2-proxy/oauth2-proxy/pkg/encryption"
1515 "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
1616)
1717
1818const (
19− // Cookies are limited to 4kb including the length of the cookie name,
20− // the cookie name can be up to 256 bytes
21− maxCookieLength = 3840
19+ // Cookies are limited to 4kb for all parts
20+ // including the cookie name, value, attributes; IE (http.cookie).String()
21+ // Most browsers' max is 4096 -- but we give ourselves some leeway
22+ maxCookieLength = 4000
2223)
2324
2425// Ensure CookieSessionStore implements the interface
@@ -107,14 +108,15 @@ func (s *SessionStore) makeSessionCookie(req *http.Request, value string, now ti
107108 value = encryption.SignedValue(s.CookieOptions.Secret, s.CookieOptions.Name, []byte(value), now)
108109 }
109110 c := s.makeCookie(req, s.CookieOptions.Name, value, s.CookieOptions.Expire, now)
110− if len(c.Value) > 4096-len(s.CookieOptions.Name) {
111+
112+ if len(c.String()) > maxCookieLength {
111113 return splitCookie(c)
112114 }
113115 return []*http.Cookie{c}
114116}
115117
116118func (s *SessionStore) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {
117− return cookies.MakeCookieFromOptions(
119+ return pkgcookies.MakeCookieFromOptions(
118120 req,
119121 name,
120122 value,
@@ -142,23 +144,30 @@ func NewCookieSessionStore(opts *options.SessionOptions, cookieOpts *options.Coo
142144// it into a slice of cookies which fit within the 4kb cookie limit indexing
143145// the cookies from 0
144146func splitCookie(c *http.Cookie) []*http.Cookie {
145− logger.Printf("WARNING: Multiple cookies are required for this session as it exceeds the 4kb cookie limit. Please use server side session storage (eg. Redis) instead.")
146− if len(c.Value) < maxCookieLength {
147+ if len(c.String()) < maxCookieLength {
147148 return []*http.Cookie{c}
148149 }
150+
151+ logger.Printf("WARNING: Multiple cookies are required for this session as it exceeds the 4kb cookie limit. Please use server side session storage (eg. Redis) instead.")
152+
149153 cookies := []*http.Cookie{}
150154 valueBytes := []byte(c.Value)
151155 count := 0
152156 for len(valueBytes) > 0 {
153157 newCookie := copyCookie(c)
154− newCookie.Name = fmt.Sprintf("%s_%d", c.Name, count)
158+ newCookie.Name = splitCookieName(c.Name, count)
155159 count++
156− if len(valueBytes) < maxCookieLength {
157− newCookie.Value = string(valueBytes)
160+
161+ newCookie.Value = string(valueBytes)
162+ cookieLength := len(newCookie.String())
163+ if cookieLength <= maxCookieLength {
158164 valueBytes = []byte{}
159165 } else {
160− newValue := valueBytes[:maxCookieLength]
161− valueBytes = valueBytes[maxCookieLength:]
166+ overflow := cookieLength - maxCookieLength
167+ valueSize := len(valueBytes) - overflow
168+
169+ newValue := valueBytes[:valueSize]
170+ valueBytes = valueBytes[valueSize:]
162171 newCookie.Value = string(newValue)
163172 }
164173 cookies = append(cookies, newCookie)
@@ -166,6 +175,15 @@ func splitCookie(c *http.Cookie) []*http.Cookie {
166175 return cookies
167176}
168177
178+func splitCookieName(name string, count int) string {
179+ splitName := fmt.Sprintf("%s_%d", name, count)
180+ overflow := len(splitName) - 256
181+ if overflow > 0 {
182+ splitName = fmt.Sprintf("%s_%d", name[:len(name)-overflow], count)
183+ }
184+ return splitName
185+}
186+
169187// loadCookie retreieves the sessions state cookie from the http request.
170188// If a single cookie is present this will be returned, otherwise it attempts
171189// to reconstruct a cookie split up by splitCookie
@@ -179,7 +197,7 @@ func loadCookie(req *http.Request, cookieName string) (*http.Cookie, error) {
179197 count := 0
180198 for err == nil {
181199 var c *http.Cookie
182− c, err = req.Cookie(fmt.Sprintf("%s_%d", cookieName, count))
200+ c, err = req.Cookie(splitCookieName(cookieName, count))
183201 if err == nil {
184202 cookies = append(cookies, c)
185203 count++
pkg/sessions/cookie/session_store_test.go+111
@@ -1,7 +1,10 @@
11package cookie
22
33import (
4+ "fmt"
5+ mathrand "math/rand"
46 "net/http"
7+ "strings"
58 "testing"
69 "time"
710
@@ -49,3 +52,111 @@ func Test_copyCookie(t *testing.T) {
4952 got := copyCookie(c)
5053 assert.Equal(t, c, got)
5154}
55+
56+func Test_splitCookie(t *testing.T) {
57+ testCases := map[string]*http.Cookie{
58+ "Short cookie name": {
59+ Name: "short",
60+ Value: strings.Repeat("v", 10000),
61+ },
62+ "Long cookie name": {
63+ Name: strings.Repeat("n", 251),
64+ Value: strings.Repeat("a", 10000),
65+ },
66+ "Max cookie name": {
67+ Name: strings.Repeat("n", 256),
68+ Value: strings.Repeat("a", 10000),
69+ },
70+ "Suffix overflow cookie name": {
71+ Name: strings.Repeat("n", 255),
72+ Value: strings.Repeat("a", 10000),
73+ },
74+ "Double digit suffix cookie name overflow": {
75+ Name: strings.Repeat("n", 253),
76+ Value: strings.Repeat("a", 50000),
77+ },
78+ "With short name and attributes": {
79+ Name: "short",
80+ Value: strings.Repeat("v", 10000),
81+ Path: "/path",
82+ Domain: "x.y.z",
83+ Secure: true,
84+ HttpOnly: true,
85+ SameSite: http.SameSiteLaxMode,
86+ },
87+ "With max length name and attributes": {
88+ Name: strings.Repeat("n", 256),
89+ Value: strings.Repeat("v", 10000),
90+ Path: "/path",
91+ Domain: "x.y.z",
92+ Secure: true,
93+ HttpOnly: true,
94+ SameSite: http.SameSiteLaxMode,
95+ },
96+ }
97+ for testName, tc := range testCases {
98+ t.Run(testName, func(t *testing.T) {
99+ splitCookies := splitCookie(tc)
100+ for i, cookie := range splitCookies {
101+ if i < len(splitCookies)-1 {
102+ assert.Equal(t, 4000, len(cookie.String()))
103+ } else {
104+ assert.GreaterOrEqual(t, 4000, len(cookie.String()))
105+ }
106+ }
107+ })
108+ }
109+}
110+
111+func Test_splitCookieName(t *testing.T) {
112+ testCases := map[string]struct {
113+ Name string
114+ Count int
115+ Output string
116+ }{
117+ "Standard length": {
118+ Name: "IAmSoNormal",
119+ Count: 2,
120+ Output: "IAmSoNormal_2",
121+ },
122+ "Max length": {
123+ Name: strings.Repeat("n", 256),
124+ Count: 1,
125+ Output: fmt.Sprintf("%s_%d", strings.Repeat("n", 254), 1),
126+ },
127+ "Large count overflow": {
128+ Name: strings.Repeat("n", 253),
129+ Count: 1000,
130+ Output: fmt.Sprintf("%s_%d", strings.Repeat("n", 251), 1000),
131+ },
132+ }
133+ for testName, tc := range testCases {
134+ t.Run(testName, func(t *testing.T) {
135+ splitName := splitCookieName(tc.Name, tc.Count)
136+ assert.Equal(t, tc.Output, splitName)
137+ })
138+ }
139+}
140+
141+func Test_splitCookie_joinCookies(t *testing.T) {
142+ const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
143+
144+ v := make([]byte, 251)
145+ for i := range v {
146+ v[i] = charset[mathrand.Intn(len(charset))]
147+ }
148+ value := strings.Repeat(string(v), 1000)
149+
150+ for _, nameSize := range []int{1, 10, 50, 100, 200, 254} {
151+ t.Run(fmt.Sprintf("%d length cookie name", nameSize), func(t *testing.T) {
152+ cookie := &http.Cookie{
153+ Name: strings.Repeat("n", nameSize),
154+ Value: value,
155+ }
156+ splitCookies := splitCookie(cookie)
157+ joinedCookie, err := joinCookies(splitCookies)
158+ assert.NoError(t, err)
159+ assert.Equal(t, *cookie, *joinedCookie)
160+ })
161+ }
162+}