You've already forked oauth2-proxy
mirror of
https://github.com/oauth2-proxy/oauth2-proxy.git
synced 2025-08-06 22:42:56 +02:00
feat(cookie) csrf per request limit (#3134)
* Allow setting maximum number of csrf cookies, deleting the oldest if necessary * Add a test for multiple CSRF cookies to remove the old cookie * Add docs/changelog * If limit is <=0 do not clear Signed-off-by: test <bert@transtrend.com> * Better docs Co-authored-by: Jan Larwig <jan@larwig.com> * direct check of option value Co-authored-by: Jan Larwig <jan@larwig.com> * direct use of option value Co-authored-by: Jan Larwig <jan@larwig.com> * sort based on clock compare vs time compare Co-authored-by: Jan Larwig <jan@larwig.com> * clock.Clock does not implement Compare, fix csrf cookie extraction after rename Signed-off-by: Bert Helderman <bert@transtrend.com> * Linter fix * add method signature documentation and slight formatting Signed-off-by: Jan Larwig <jan@larwig.com> * fix: test case for csrf cookie limit and flag Signed-off-by: Jan Larwig <jan@larwig.com> --------- Signed-off-by: Bert Helderman <bert@transtrend.com> Signed-off-by: Jan Larwig <jan@larwig.com> Co-authored-by: test <bert@transtrend.com> Co-authored-by: bh-tt <71650427+bh-tt@users.noreply.github.com>
This commit is contained in:
@ -4,6 +4,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
|
||||
@ -151,6 +153,48 @@ func (c *csrf) SetCookie(rw http.ResponseWriter, req *http.Request) (*http.Cooki
|
||||
return cookie, nil
|
||||
}
|
||||
|
||||
// ClearExtraCsrfCookies limits the amount of existing CSRF cookies by deleting
|
||||
// an excess of cookies controlled through the option CSRFPerRequestLimit
|
||||
func ClearExtraCsrfCookies(opts *options.Cookie, rw http.ResponseWriter, req *http.Request) {
|
||||
if !opts.CSRFPerRequest || opts.CSRFPerRequestLimit <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
cookies := req.Cookies()
|
||||
existingCsrfCookies := []*http.Cookie{}
|
||||
startsWith := fmt.Sprintf("%v_", opts.Name)
|
||||
|
||||
// determine how many csrf cookies we have
|
||||
for _, cookie := range cookies {
|
||||
if strings.HasPrefix(cookie.Name, startsWith) && strings.HasSuffix(cookie.Name, "_csrf") {
|
||||
existingCsrfCookies = append(existingCsrfCookies, cookie)
|
||||
}
|
||||
}
|
||||
|
||||
// short circuit return
|
||||
if len(existingCsrfCookies) <= opts.CSRFPerRequestLimit {
|
||||
return
|
||||
}
|
||||
|
||||
decodedCookies := []*csrf{}
|
||||
for _, cookie := range existingCsrfCookies {
|
||||
decodedCookie, err := decodeCSRFCookie(cookie, opts)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
decodedCookies = append(decodedCookies, decodedCookie)
|
||||
}
|
||||
|
||||
// delete the X oldest cookies
|
||||
slices.SortStableFunc(decodedCookies, func(a, b *csrf) int {
|
||||
return a.time.Now().Compare(b.time.Now())
|
||||
})
|
||||
|
||||
for i := 0; i < len(decodedCookies)-opts.CSRFPerRequestLimit; i++ {
|
||||
decodedCookies[i].ClearCookie(rw, req)
|
||||
}
|
||||
}
|
||||
|
||||
// ClearCookie removes the CSRF cookie
|
||||
func (c *csrf) ClearCookie(rw http.ResponseWriter, req *http.Request) {
|
||||
http.SetCookie(rw, MakeCookieFromOptions(
|
||||
@ -181,7 +225,7 @@ func (c *csrf) encodeCookie() (string, error) {
|
||||
// decodeCSRFCookie validates the signature then decrypts and decodes a CSRF
|
||||
// cookie into a CSRF struct
|
||||
func decodeCSRFCookie(cookie *http.Cookie, opts *options.Cookie) (*csrf, error) {
|
||||
val, _, ok := encryption.Validate(cookie, opts.Secret, opts.Expire)
|
||||
val, t, ok := encryption.Validate(cookie, opts.Secret, opts.Expire)
|
||||
if !ok {
|
||||
return nil, errors.New("CSRF cookie failed validation")
|
||||
}
|
||||
@ -192,7 +236,9 @@ func decodeCSRFCookie(cookie *http.Cookie, opts *options.Cookie) (*csrf, error)
|
||||
}
|
||||
|
||||
// Valid cookie, Unmarshal the CSRF
|
||||
csrf := &csrf{cookieOpts: opts}
|
||||
clock := clock.Clock{}
|
||||
clock.Set(t)
|
||||
csrf := &csrf{cookieOpts: opts, time: clock}
|
||||
err = msgpack.Unmarshal(decrypted, csrf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error unmarshalling data to CSRF: %v", err)
|
||||
|
@ -190,5 +190,84 @@ var _ = Describe("CSRF Cookie with non-fixed name Tests", func() {
|
||||
Expect(privateCSRF.cookieName()).To(ContainSubstring(cookieName))
|
||||
})
|
||||
})
|
||||
|
||||
Context("CSRF per request limit", func() {
|
||||
It("clears cookies based on the limit", func() {
|
||||
//needs to be now as pkg/encryption/utils.go uses time.Now()
|
||||
testNow := time.Now()
|
||||
cookieOpts.CSRFPerRequestLimit = 1
|
||||
|
||||
publicCSRF1, err := NewCSRF(cookieOpts, "verifier")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
privateCSRF1 := publicCSRF1.(*csrf)
|
||||
privateCSRF1.time.Set(testNow)
|
||||
|
||||
publicCSRF2, err := NewCSRF(cookieOpts, "verifier")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
privateCSRF2 := publicCSRF2.(*csrf)
|
||||
privateCSRF2.time.Set(testNow.Add(time.Minute))
|
||||
|
||||
publicCSRF3, err := NewCSRF(cookieOpts, "verifier")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
privateCSRF3 := publicCSRF3.(*csrf)
|
||||
privateCSRF3.time.Set(testNow.Add(time.Minute * 2))
|
||||
|
||||
cookies := []string{}
|
||||
for _, csrf := range []*csrf{privateCSRF1, privateCSRF2, privateCSRF3} {
|
||||
encoded, err := csrf.encodeCookie()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
cookie := MakeCookieFromOptions(
|
||||
req,
|
||||
csrf.cookieName(),
|
||||
encoded,
|
||||
csrf.cookieOpts,
|
||||
csrf.cookieOpts.CSRFExpire,
|
||||
)
|
||||
cookies = append(cookies, fmt.Sprintf("%v=%v", cookie.Name, cookie.Value))
|
||||
}
|
||||
|
||||
header := make(map[string][]string, 1)
|
||||
header["Cookie"] = cookies
|
||||
req = &http.Request{
|
||||
Method: http.MethodGet,
|
||||
Proto: "HTTP/1.1",
|
||||
Host: cookieDomain,
|
||||
|
||||
URL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: cookieDomain,
|
||||
Path: cookiePath,
|
||||
},
|
||||
Header: header,
|
||||
}
|
||||
|
||||
// when setting the limit to one csrf cookie but configuring three csrf cookies
|
||||
// then two cookies should be removed / set to expired on the response
|
||||
|
||||
// for this test case we have set all the cookies on a single request,
|
||||
// but in reality this will be multiple requests after another
|
||||
rw := httptest.NewRecorder()
|
||||
ClearExtraCsrfCookies(cookieOpts, rw, req)
|
||||
|
||||
clearedCookies := rw.Header()["Set-Cookie"]
|
||||
Expect(clearedCookies).To(HaveLen(2))
|
||||
Expect(clearedCookies[0]).To(Equal(
|
||||
fmt.Sprintf(
|
||||
"%s=; Path=%s; Domain=%s; Max-Age=0; HttpOnly; Secure",
|
||||
privateCSRF1.cookieName(),
|
||||
cookiePath,
|
||||
cookieDomain,
|
||||
),
|
||||
))
|
||||
Expect(clearedCookies[1]).To(Equal(
|
||||
fmt.Sprintf(
|
||||
"%s=; Path=%s; Domain=%s; Max-Age=0; HttpOnly; Secure",
|
||||
privateCSRF2.cookieName(),
|
||||
cookiePath,
|
||||
cookieDomain,
|
||||
),
|
||||
))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
Reference in New Issue
Block a user