Skip to content

Commit 85a5843

Browse files
committed
[PLT-1263] Add a cookie-csrf-per-request-limit attribute
1 parent c36cf84 commit 85a5843

File tree

7 files changed

+142
-26
lines changed

7 files changed

+142
-26
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
### 7.5.1-0.3.0 (2023-11-24)
44

5+
* [PLT-1263] Add a cookie-csrf-per-request-limit attribute
6+
7+
### 7.5.1-0.3.0 (2023-11-24)
8+
59
* [EOS-12032] Use jwt session store
610

711
## Previous development
@@ -31,4 +35,4 @@
3135

3236
* Add tenant and groups to userinfo
3337
* Add SIS provider and JWT session support
34-
* Adapt repo to Stratio CICD flow
38+
* Adapt repo to Stratio CICD flow

Jenkinsfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ hose {
88
ANCHORE_POLICY = "production"
99
VERSIONING_TYPE = 'stratioVersion-3-3'
1010
UPSTREAM_VERSION = '7.5.1'
11+
DEPLOYONPRS = true
1112
GRYPE_TEST = false
1213

1314
DEV = { config ->

docs/docs/configuration/overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ An example [oauth2-proxy.cfg](https://github.com/oauth2-proxy/oauth2-proxy/blob/
9999
| `--cookie-samesite` | string | set SameSite cookie attribute (`"lax"`, `"strict"`, `"none"`, or `""`). | `""` |
100100
| `--cookie-csrf-per-request` | bool | Enable having different CSRF cookies per request, making it possible to have parallel requests. | false |
101101
| `--cookie-csrf-expire` | duration | expire timeframe for CSRF cookie | 15m |
102+
| `--cookie-csrf-per-request-limit` | int | Sets a limit on the number of CSRF requests cookies that oauth2-proxy will create. The oldest cookie will be removed. Useful if users end up with 431 Request headers too large status codes. Only effective if --cookie-csrf-per-request is true | "infinite" |
102103
| `--custom-templates-dir` | string | path to custom html templates | |
103104
| `--custom-sign-in-logo` | string | path or a URL to an custom image for the sign_in page logo. Use `"-"` to disable default logo. |
104105
| `--display-htpasswd-form` | bool | display username / password login form if an htpasswd file is provided | true |

oauthproxy.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,7 @@ func (p *OAuthProxy) doOAuthStart(rw http.ResponseWriter, req *http.Request, ove
817817
extraParams,
818818
)
819819

820+
cookies.ClearExtraCsrfCookies(p.CookieOptions, rw, req)
820821
if _, err := csrf.SetCookie(rw, req); err != nil {
821822
logger.Errorf("Error setting CSRF cookie: %v", err)
822823
p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())

pkg/apis/options/cookie.go

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,18 @@ import (
88

99
// Cookie contains configuration options relating to Cookie configuration
1010
type Cookie struct {
11-
Name string `flag:"cookie-name" cfg:"cookie_name"`
12-
Secret string `flag:"cookie-secret" cfg:"cookie_secret"`
13-
Domains []string `flag:"cookie-domain" cfg:"cookie_domains"`
14-
Path string `flag:"cookie-path" cfg:"cookie_path"`
15-
Expire time.Duration `flag:"cookie-expire" cfg:"cookie_expire"`
16-
Refresh time.Duration `flag:"cookie-refresh" cfg:"cookie_refresh"`
17-
Secure bool `flag:"cookie-secure" cfg:"cookie_secure"`
18-
HTTPOnly bool `flag:"cookie-httponly" cfg:"cookie_httponly"`
19-
SameSite string `flag:"cookie-samesite" cfg:"cookie_samesite"`
20-
CSRFPerRequest bool `flag:"cookie-csrf-per-request" cfg:"cookie_csrf_per_request"`
21-
CSRFExpire time.Duration `flag:"cookie-csrf-expire" cfg:"cookie_csrf_expire"`
11+
Name string `flag:"cookie-name" cfg:"cookie_name"`
12+
Secret string `flag:"cookie-secret" cfg:"cookie_secret"`
13+
Domains []string `flag:"cookie-domain" cfg:"cookie_domains"`
14+
Path string `flag:"cookie-path" cfg:"cookie_path"`
15+
Expire time.Duration `flag:"cookie-expire" cfg:"cookie_expire"`
16+
Refresh time.Duration `flag:"cookie-refresh" cfg:"cookie_refresh"`
17+
Secure bool `flag:"cookie-secure" cfg:"cookie_secure"`
18+
HTTPOnly bool `flag:"cookie-httponly" cfg:"cookie_httponly"`
19+
SameSite string `flag:"cookie-samesite" cfg:"cookie_samesite"`
20+
CSRFPerRequest bool `flag:"cookie-csrf-per-request" cfg:"cookie_csrf_per_request"`
21+
CSRFExpire time.Duration `flag:"cookie-csrf-expire" cfg:"cookie_csrf_expire"`
22+
CSRFPerRequestLimit int `flag:"cookie-csrf-per-request-limit" cfg:"cookie_csrf_per_request_limit"`
2223
}
2324

2425
func cookieFlagSet() *pflag.FlagSet {
@@ -35,22 +36,24 @@ func cookieFlagSet() *pflag.FlagSet {
3536
flagSet.String("cookie-samesite", "", "set SameSite cookie attribute (ie: \"lax\", \"strict\", \"none\", or \"\"). ")
3637
flagSet.Bool("cookie-csrf-per-request", false, "When this property is set to true, then the CSRF cookie name is built based on the state and varies per request. If property is set to false, then CSRF cookie has the same name for all requests.")
3738
flagSet.Duration("cookie-csrf-expire", time.Duration(15)*time.Minute, "expire timeframe for CSRF cookie")
39+
flagSet.Int("cookie-csrf-per-request-limit", 0, "default value for CSRFPerRequestLimit key")
3840
return flagSet
3941
}
4042

4143
// cookieDefaults creates a Cookie populating each field with its default value
4244
func cookieDefaults() Cookie {
4345
return Cookie{
44-
Name: "_oauth2_proxy",
45-
Secret: "",
46-
Domains: nil,
47-
Path: "/",
48-
Expire: time.Duration(168) * time.Hour,
49-
Refresh: time.Duration(0),
50-
Secure: true,
51-
HTTPOnly: true,
52-
SameSite: "",
53-
CSRFPerRequest: false,
54-
CSRFExpire: time.Duration(15) * time.Minute,
46+
Name: "_oauth2_proxy",
47+
Secret: "",
48+
Domains: nil,
49+
Path: "/",
50+
Expire: time.Duration(168) * time.Hour,
51+
Refresh: time.Duration(0),
52+
Secure: true,
53+
HTTPOnly: true,
54+
SameSite: "",
55+
CSRFPerRequest: false,
56+
CSRFExpire: time.Duration(15) * time.Minute,
57+
CSRFPerRequestLimit: 0,
5558
}
56-
}
59+
}

pkg/cookies/csrf.go

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"errors"
55
"fmt"
66
"net/http"
7+
"sort"
8+
"strings"
79
"time"
810

911
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
@@ -146,6 +148,43 @@ func (c *csrf) SetCookie(rw http.ResponseWriter, req *http.Request) (*http.Cooki
146148
return cookie, nil
147149
}
148150

151+
func ClearExtraCsrfCookies(opts *options.Cookie, rw http.ResponseWriter, req *http.Request) {
152+
if !opts.CSRFPerRequest || opts.CSRFPerRequestLimit <= 0 {
153+
return
154+
}
155+
156+
cookies := req.Cookies()
157+
existingCsrfCookies := []*http.Cookie{}
158+
startsWith := fmt.Sprintf("%v_", opts.Name)
159+
for _, cookie := range cookies {
160+
if strings.HasPrefix(cookie.Name, startsWith) && strings.Contains(cookie.Name, "_csrf_") {
161+
existingCsrfCookies = append(existingCsrfCookies, cookie)
162+
}
163+
}
164+
165+
if len(existingCsrfCookies) <= opts.CSRFPerRequestLimit {
166+
return
167+
}
168+
169+
decodedCookies := []*csrf{}
170+
for _, cookie := range existingCsrfCookies {
171+
decodedCookie, err := decodeCSRFCookie(cookie, opts)
172+
if err != nil {
173+
continue
174+
}
175+
decodedCookies = append(decodedCookies, decodedCookie)
176+
}
177+
178+
sort.Slice(decodedCookies, func(i, j int) bool {
179+
return decodedCookies[i].time.Now().Before(decodedCookies[j].time.Now())
180+
})
181+
182+
numberToDelete := len(decodedCookies) - opts.CSRFPerRequestLimit
183+
for i := 0; i < numberToDelete; i++ {
184+
decodedCookies[i].ClearCookie(rw, req)
185+
}
186+
}
187+
149188
// ClearCookie removes the CSRF cookie
150189
func (c *csrf) ClearCookie(rw http.ResponseWriter, req *http.Request) {
151190
http.SetCookie(rw, MakeCookieFromOptions(
@@ -177,7 +216,7 @@ func (c *csrf) encodeCookie() (string, error) {
177216
// decodeCSRFCookie validates the signature then decrypts and decodes a CSRF
178217
// cookie into a CSRF struct
179218
func decodeCSRFCookie(cookie *http.Cookie, opts *options.Cookie) (*csrf, error) {
180-
val, _, ok := encryption.Validate(cookie, opts.Secret, opts.Expire)
219+
val, t, ok := encryption.Validate(cookie, opts.Secret, opts.Expire)
181220
if !ok {
182221
return nil, errors.New("CSRF cookie failed validation")
183222
}
@@ -188,7 +227,9 @@ func decodeCSRFCookie(cookie *http.Cookie, opts *options.Cookie) (*csrf, error)
188227
}
189228

190229
// Valid cookie, Unmarshal the CSRF
191-
csrf := &csrf{cookieOpts: opts}
230+
clock := clock.Clock{}
231+
clock.Set(t)
232+
csrf := &csrf{cookieOpts: opts, time: clock}
192233
err = msgpack.Unmarshal(decrypted, csrf)
193234
if err != nil {
194235
return nil, fmt.Errorf("error unmarshalling data to CSRF: %v", err)

pkg/cookies/csrf_per_request_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,5 +191,70 @@ var _ = Describe("CSRF Cookie with non-fixed name Tests", func() {
191191
Expect(privateCSRF.cookieName()).To(ContainSubstring(cookieName))
192192
})
193193
})
194+
Context("CSRF max cookies", func() {
195+
It("disables the 3rd cookie if a limit of 2 is set", func() {
196+
//needs to be now as pkg/encryption/utils.go uses time.Now()
197+
testNow := time.Now()
198+
cookieOpts.CSRFPerRequestLimit = 2
199+
200+
publicCSRF1, err := NewCSRF(cookieOpts, "verifier")
201+
Expect(err).ToNot(HaveOccurred())
202+
privateCSRF1 := publicCSRF1.(*csrf)
203+
privateCSRF1.time.Set(testNow)
204+
205+
publicCSRF2, err := NewCSRF(cookieOpts, "verifier")
206+
Expect(err).ToNot(HaveOccurred())
207+
privateCSRF2 := publicCSRF2.(*csrf)
208+
privateCSRF2.time.Set(testNow.Add(time.Minute))
209+
210+
publicCSRF3, err := NewCSRF(cookieOpts, "verifier")
211+
Expect(err).ToNot(HaveOccurred())
212+
privateCSRF3 := publicCSRF3.(*csrf)
213+
privateCSRF3.time.Set(testNow.Add(time.Minute * 2))
214+
215+
//for the test we set all the cookies on a single request, but in reality this will be multiple requests after another
216+
cookies := []string{}
217+
for _, csrf := range []*csrf{privateCSRF1, privateCSRF2, privateCSRF3} {
218+
encoded, err := csrf.encodeCookie()
219+
Expect(err).ToNot(HaveOccurred())
220+
cookie := MakeCookieFromOptions(
221+
req,
222+
csrf.cookieName(),
223+
encoded,
224+
csrf.cookieOpts,
225+
csrf.cookieOpts.CSRFExpire,
226+
csrf.time.Now(),
227+
)
228+
cookies = append(cookies, fmt.Sprintf("%v=%v", cookie.Name, cookie.Value))
229+
}
230+
header := make(map[string][]string, 1)
231+
header["Cookie"] = cookies
232+
req = &http.Request{
233+
Method: http.MethodGet,
234+
Proto: "HTTP/1.1",
235+
Host: cookieDomain,
236+
237+
URL: &url.URL{
238+
Scheme: "https",
239+
Host: cookieDomain,
240+
Path: cookiePath,
241+
},
242+
Header: header,
243+
}
244+
fmt.Println(req.Cookies())
245+
rw := httptest.NewRecorder()
246+
ClearExtraCsrfCookies(cookieOpts, rw, req)
247+
248+
Expect(rw.Header().Values("Set-Cookie")).To(ContainElement(
249+
fmt.Sprintf(
250+
"%s=; Path=%s; Domain=%s; Expires=%s; HttpOnly; Secure",
251+
privateCSRF1.cookieName(),
252+
cookiePath,
253+
cookieDomain,
254+
testCookieExpires(testNow.Add(time.Hour*-1)),
255+
),
256+
))
257+
})
258+
})
194259
})
195260
})

0 commit comments

Comments
 (0)