1
0
mirror of https://github.com/oauth2-proxy/oauth2-proxy.git synced 2025-08-08 22:46:33 +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:
Jan Larwig
2025-07-20 16:44:42 +02:00
committed by GitHub
parent d5f8507cc8
commit b57c82181d
6 changed files with 186 additions and 56 deletions

View File

@ -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)