From bcd5ac513c60988a97cf6ca62c0163f6db250e15 Mon Sep 17 00:00:00 2001 From: Joel Speed Date: Sat, 27 Jan 2018 22:48:52 +0000 Subject: [PATCH] Split large cookies --- oauthproxy.go | 104 ++++++++++++++++++++++++++++++++++++++++----- oauthproxy_test.go | 79 ++++++++++++++++++++++++++++++++-- 2 files changed, 168 insertions(+), 15 deletions(-) diff --git a/oauthproxy.go b/oauthproxy.go index 62b6eb1..34d8495 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -260,15 +260,92 @@ func (p *OAuthProxy) redeemCode(host, code string) (s *providers.SessionState, e return } -func (p *OAuthProxy) MakeSessionCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie { +func (p *OAuthProxy) MakeSessionCookie(req *http.Request, value string, expiration time.Duration, now time.Time) []*http.Cookie { if value != "" { value = cookie.SignedValue(p.CookieSeed, p.CookieName, value, now) - if len(value) > 4096 { - // Cookies cannot be larger than 4kb - log.Printf("WARNING - Cookie Size: %d bytes", len(value)) + } + c := p.makeCookie(req, p.CookieName, value, expiration, now) + if len(c.Value) > 4096 { + return splitCookie(c) + } + return []*http.Cookie{c} +} + +func copyCookie(c *http.Cookie) *http.Cookie { + return &http.Cookie{ + Name: c.Name, + Value: c.Value, + Path: c.Path, + Domain: c.Domain, + Expires: c.Expires, + RawExpires: c.RawExpires, + MaxAge: c.MaxAge, + Secure: c.Secure, + HttpOnly: c.HttpOnly, + Raw: c.Raw, + Unparsed: c.Unparsed, + } +} + +func splitCookie(c *http.Cookie) []*http.Cookie { + if len(c.Value) < 3840 { + return []*http.Cookie{c} + } + cookies := []*http.Cookie{} + valueBytes := []byte(c.Value) + count := 0 + for len(valueBytes) > 0 { + new := copyCookie(c) + new.Name = fmt.Sprintf("%s-%d", c.Name, count) + count++ + if len(valueBytes) < 3840 { + new.Value = string(valueBytes) + valueBytes = []byte{} + } else { + newValue := valueBytes[:3840] + valueBytes = valueBytes[3840:] + new.Value = string(newValue) + } + cookies = append(cookies, new) + } + return cookies +} + +func joinCookies(cookies []*http.Cookie) (*http.Cookie, error) { + if len(cookies) == 0 { + return nil, fmt.Errorf("Could not load cookie.") + } + if len(cookies) == 1 { + return cookies[0], nil + } + c := copyCookie(cookies[0]) + for i := 1; i < len(cookies); i++ { + c.Value += cookies[i].Value + } + c.Name = strings.TrimRight(c.Name, "-0") + return c, nil +} + +func loadCookie(req *http.Request, cookieName string) (*http.Cookie, error) { + c, err := req.Cookie(cookieName) + if err == nil { + return c, nil + } + cookies := []*http.Cookie{} + err = nil + count := 0 + for err == nil { + var c *http.Cookie + c, err = req.Cookie(fmt.Sprintf("%s-%d", cookieName, count)) + if err == nil { + cookies = append(cookies, c) + count++ } } - return p.makeCookie(req, p.CookieName, value, expiration, now) + if len(cookies) == 0 { + return nil, fmt.Errorf("Could not find cookie %s", cookieName) + } + return joinCookies(cookies) } func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie { @@ -298,6 +375,7 @@ func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, ex } func (p *OAuthProxy) ClearCSRFCookie(rw http.ResponseWriter, req *http.Request) { + http.SetCookie(rw, p.MakeCSRFCookie(req, "", time.Hour*-1, time.Now())) } @@ -306,24 +384,28 @@ func (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, va } func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) { - clr := p.MakeSessionCookie(req, "", time.Hour*-1, time.Now()) - http.SetCookie(rw, clr) + cookies := p.MakeSessionCookie(req, "", time.Hour*-1, time.Now()) + for _, clr := range cookies { + http.SetCookie(rw, clr) + } // ugly hack because default domain changed - if p.CookieDomain == "" { - clr2 := *clr + if p.CookieDomain == "" && len(cookies) > 0 { + clr2 := *cookies[0] clr2.Domain = req.Host http.SetCookie(rw, &clr2) } } func (p *OAuthProxy) SetSessionCookie(rw http.ResponseWriter, req *http.Request, val string) { - http.SetCookie(rw, p.MakeSessionCookie(req, val, p.CookieExpire, time.Now())) + for _, c := range p.MakeSessionCookie(req, val, p.CookieExpire, time.Now()) { + http.SetCookie(rw, c) + } } func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*providers.SessionState, time.Duration, error) { var age time.Duration - c, err := req.Cookie(p.CookieName) + c, err := loadCookie(req, p.CookieName) if err != nil { // always http.ErrNoCookie return nil, age, fmt.Errorf("Cookie %q not present", p.CookieName) diff --git a/oauthproxy_test.go b/oauthproxy_test.go index 875c39c..3fb8fd2 100644 --- a/oauthproxy_test.go +++ b/oauthproxy_test.go @@ -6,6 +6,7 @@ import ( "io" "io/ioutil" "log" + "math/rand" "net" "net/http" "net/http/httptest" @@ -143,6 +144,73 @@ func TestIsValidRedirect(t *testing.T) { assert.Equal(t, false, invalidHttps2) } +func randomString(length int) string { + charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + seededRand := rand.New(rand.NewSource(time.Now().UnixNano())) + b := make([]byte, length) + for i := range b { + b[i] = charset[seededRand.Intn(len(charset))] + } + return string(b) +} + +func TestSplitCookie(t *testing.T) { + c1 := &http.Cookie{ + Name: "cookie-name", + Value: randomString(5120), + Path: "/", + Domain: "foo.bar", + HttpOnly: true, + Secure: true, + Expires: time.Now(), + } + cookies := splitCookie(c1) + assert.Equal(t, 2, len(cookies)) + + assert.Equal(t, c1.Name+"-0", cookies[0].Name) + assert.Equal(t, c1.Name+"-1", cookies[1].Name) + + assert.Equal(t, 3840, len(cookies[0].Value)) + assert.Equal(t, 5120-3840, len(cookies[1].Value)) + + c2 := &http.Cookie{ + Name: "cookie-name", + Value: randomString(3000), + Path: "/", + Domain: "foo.bar", + HttpOnly: true, + Secure: true, + Expires: time.Now(), + } + + cookies2 := splitCookie(c2) + assert.Equal(t, 1, len(cookies2)) + + assert.Equal(t, c2.Name, cookies2[0].Name) + assert.Equal(t, c2.Value, cookies2[0].Value) +} + +func TestJoinCookies(t *testing.T) { + c1 := &http.Cookie{ + Name: "cookie-name", + Value: randomString(5120), + Path: "/", + Domain: "foo.bar", + HttpOnly: true, + Secure: true, + Expires: time.Now(), + } + // Split Cookies + cookies := splitCookie(c1) + assert.Equal(t, 2, len(cookies)) + + // join cookies should be the ivnerse + c2, _ := joinCookies(cookies) + + assert.Equal(t, c1.Name, c2.Name) + assert.Equal(t, c1.Value, c2.Value) +} + type TestProvider struct { *providers.ProviderData EmailAddress string @@ -555,7 +623,7 @@ func NewProcessCookieTestWithDefaults() *ProcessCookieTest { }) } -func (p *ProcessCookieTest) MakeCookie(value string, ref time.Time) *http.Cookie { +func (p *ProcessCookieTest) MakeCookie(value string, ref time.Time) []*http.Cookie { return p.proxy.MakeSessionCookie(p.req, value, p.opts.CookieExpire, ref) } @@ -564,7 +632,9 @@ func (p *ProcessCookieTest) SaveSession(s *providers.SessionState, ref time.Time if err != nil { return err } - p.req.AddCookie(p.proxy.MakeSessionCookie(p.req, value, p.proxy.CookieExpire, ref)) + for _, c := range p.proxy.MakeSessionCookie(p.req, value, p.proxy.CookieExpire, ref) { + p.req.AddCookie(c) + } return nil } @@ -853,8 +923,9 @@ func (st *SignatureTest) MakeRequestWithExpectedKey(method, body, key string) { if err != nil { panic(err) } - cookie := proxy.MakeSessionCookie(req, value, proxy.CookieExpire, time.Now()) - req.AddCookie(cookie) + for _, c := range proxy.MakeSessionCookie(req, value, proxy.CookieExpire, time.Now()) { + req.AddCookie(c) + } // This is used by the upstream to validate the signature. st.authenticator.auth = hmacauth.NewHmacAuth( crypto.SHA1, []byte(key), SignatureHeader, SignatureHeaders)