Skip to content

Commit 9b261f5

Browse files
authored
Add SameSite setting for cookies (#14900)
Add SameSite setting for cookies and rationalise the cookie setting code. Switches SameSite to Lax by default. There is a possible future extension of differentiating which cookies could be set at Strict by default but that is for a future PR. Fix #5583 Signed-off-by: Andrew Thornton <[email protected]>
1 parent beed547 commit 9b261f5

File tree

14 files changed

+184
-45
lines changed

14 files changed

+184
-45
lines changed

custom/conf/app.example.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -794,6 +794,8 @@ COOKIE_SECURE = false
794794
GC_INTERVAL_TIME = 86400
795795
; Session life time in seconds, default is 86400 (1 day)
796796
SESSION_LIFE_TIME = 86400
797+
; SameSite settings. Either "none", "lax", or "strict"
798+
SAME_SITE=lax
797799

798800
[picture]
799801
AVATAR_UPLOAD_PATH = data/avatars

docs/content/doc/advanced/config-cheat-sheet.en-us.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,8 @@ Define allowed algorithms and their minimum key length (use -1 to disable a type
557557
- `COOKIE_NAME`: **i\_like\_gitea**: The name of the cookie used for the session ID.
558558
- `GC_INTERVAL_TIME`: **86400**: GC interval in seconds.
559559
- `SESSION_LIFE_TIME`: **86400**: Session life time in seconds, default is 86400 (1 day)
560+
- `DOMAIN`: **\<empty\>**: Sets the cookie Domain
561+
- `SAME_SITE`: **lax** \[strict, lax, none\]: Set the SameSite setting for the cookie.
560562

561563
## Picture (`picture`)
562564

modules/auth/sso/sso.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313

1414
"code.gitea.io/gitea/models"
1515
"code.gitea.io/gitea/modules/log"
16-
"code.gitea.io/gitea/modules/setting"
1716
"code.gitea.io/gitea/modules/web/middleware"
1817
)
1918

@@ -129,8 +128,8 @@ func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore
129128
}
130129
}
131130

132-
middleware.SetCookie(resp, "lang", user.Language, nil, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true)
131+
middleware.SetLocaleCookie(resp, user.Language, 0)
133132

134133
// Clear whatever CSRF has right now, force to generate a new one
135-
middleware.SetCookie(resp, setting.CSRFCookieName, "", -1, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true)
134+
middleware.DeleteCSRFCookie(resp)
136135
}

modules/context/auth.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"code.gitea.io/gitea/models"
1010
"code.gitea.io/gitea/modules/log"
1111
"code.gitea.io/gitea/modules/setting"
12+
"code.gitea.io/gitea/modules/web/middleware"
1213
)
1314

1415
// ToggleOptions contains required or check options
@@ -41,7 +42,7 @@ func Toggle(options *ToggleOptions) func(ctx *Context) {
4142
ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
4243
ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/change_password"
4344
if ctx.Req.URL.Path != "/user/events" {
44-
ctx.SetCookie("redirect_to", setting.AppSubURL+ctx.Req.URL.RequestURI(), 0, setting.AppSubURL)
45+
middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI())
4546
}
4647
ctx.Redirect(setting.AppSubURL + "/user/settings/change_password")
4748
return
@@ -69,7 +70,7 @@ func Toggle(options *ToggleOptions) func(ctx *Context) {
6970
if options.SignInRequired {
7071
if !ctx.IsSigned {
7172
if ctx.Req.URL.Path != "/user/events" {
72-
ctx.SetCookie("redirect_to", setting.AppSubURL+ctx.Req.URL.RequestURI(), 0, setting.AppSubURL)
73+
middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI())
7374
}
7475
ctx.Redirect(setting.AppSubURL + "/user/login")
7576
return
@@ -84,7 +85,7 @@ func Toggle(options *ToggleOptions) func(ctx *Context) {
8485
if !options.SignOutRequired && !ctx.IsSigned &&
8586
len(ctx.GetCookie(setting.CookieUserName)) > 0 {
8687
if ctx.Req.URL.Path != "/user/events" {
87-
ctx.SetCookie("redirect_to", setting.AppSubURL+ctx.Req.URL.RequestURI(), 0, setting.AppSubURL)
88+
middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI())
8889
}
8990
ctx.Redirect(setting.AppSubURL + "/user/login")
9091
return

modules/context/context.go

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -386,9 +386,28 @@ func (ctx *Context) Redirect(location string, status ...int) {
386386
http.Redirect(ctx.Resp, ctx.Req, location, code)
387387
}
388388

389-
// SetCookie set cookies to web browser
390-
func (ctx *Context) SetCookie(name string, value string, others ...interface{}) {
391-
middleware.SetCookie(ctx.Resp, name, value, others...)
389+
// SetCookie convenience function to set most cookies consistently
390+
// CSRF and a few others are the exception here
391+
func (ctx *Context) SetCookie(name, value string, expiry int) {
392+
middleware.SetCookie(ctx.Resp, name, value,
393+
expiry,
394+
setting.AppSubURL,
395+
setting.SessionConfig.Domain,
396+
setting.SessionConfig.Secure,
397+
true,
398+
middleware.SameSite(setting.SessionConfig.SameSite))
399+
}
400+
401+
// DeleteCookie convenience function to delete most cookies consistently
402+
// CSRF and a few others are the exception here
403+
func (ctx *Context) DeleteCookie(name string) {
404+
middleware.SetCookie(ctx.Resp, name, "",
405+
-1,
406+
setting.AppSubURL,
407+
setting.SessionConfig.Domain,
408+
setting.SessionConfig.Secure,
409+
true,
410+
middleware.SameSite(setting.SessionConfig.SameSite))
392411
}
393412

394413
// GetCookie returns given cookie value from request header.
@@ -399,6 +418,11 @@ func (ctx *Context) GetCookie(name string) string {
399418
// GetSuperSecureCookie returns given cookie value from request header with secret string.
400419
func (ctx *Context) GetSuperSecureCookie(secret, name string) (string, bool) {
401420
val := ctx.GetCookie(name)
421+
return ctx.CookieDecrypt(secret, val)
422+
}
423+
424+
// CookieDecrypt returns given value from with secret string.
425+
func (ctx *Context) CookieDecrypt(secret, val string) (string, bool) {
402426
if val == "" {
403427
return "", false
404428
}
@@ -414,14 +438,21 @@ func (ctx *Context) GetSuperSecureCookie(secret, name string) (string, bool) {
414438
}
415439

416440
// SetSuperSecureCookie sets given cookie value to response header with secret string.
417-
func (ctx *Context) SetSuperSecureCookie(secret, name, value string, others ...interface{}) {
441+
func (ctx *Context) SetSuperSecureCookie(secret, name, value string, expiry int) {
442+
text := ctx.CookieEncrypt(secret, value)
443+
444+
ctx.SetCookie(name, text, expiry)
445+
}
446+
447+
// CookieEncrypt encrypts a given value using the provided secret
448+
func (ctx *Context) CookieEncrypt(secret, value string) string {
418449
key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New)
419450
text, err := com.AESGCMEncrypt(key, []byte(value))
420451
if err != nil {
421452
panic("error encrypting cookie: " + err.Error())
422453
}
423454

424-
ctx.SetCookie(name, hex.EncodeToString(text), others...)
455+
return hex.EncodeToString(text)
425456
}
426457

427458
// GetCookieInt returns cookie result in int type.
@@ -533,6 +564,7 @@ func getCsrfOpts() CsrfOptions {
533564
Header: "X-Csrf-Token",
534565
CookieDomain: setting.SessionConfig.Domain,
535566
CookiePath: setting.SessionConfig.CookiePath,
567+
SameSite: setting.SessionConfig.SameSite,
536568
}
537569
}
538570

@@ -597,17 +629,17 @@ func Contexter() func(next http.Handler) http.Handler {
597629
middleware.Domain(setting.SessionConfig.Domain),
598630
middleware.HTTPOnly(true),
599631
middleware.Secure(setting.SessionConfig.Secure),
600-
//middlewares.SameSite(opt.SameSite), FIXME: we need a samesite config
632+
middleware.SameSite(setting.SessionConfig.SameSite),
601633
)
602634
return
603635
}
604636

605-
ctx.SetCookie("macaron_flash", "", -1,
637+
middleware.SetCookie(ctx.Resp, "macaron_flash", "", -1,
606638
setting.SessionConfig.CookiePath,
607639
middleware.Domain(setting.SessionConfig.Domain),
608640
middleware.HTTPOnly(true),
609641
middleware.Secure(setting.SessionConfig.Secure),
610-
//middleware.SameSite(), FIXME: we need a samesite config
642+
middleware.SameSite(setting.SessionConfig.SameSite),
611643
)
612644
})
613645

modules/context/csrf.go

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import (
2222
"net/http"
2323
"time"
2424

25+
"code.gitea.io/gitea/modules/web/middleware"
26+
2527
"github.com/unknwon/com"
2628
)
2729

@@ -37,6 +39,8 @@ type CSRF interface {
3739
GetCookiePath() string
3840
// Return the flag value used for the csrf token.
3941
GetCookieHTTPOnly() bool
42+
// Return cookie domain
43+
GetCookieDomain() string
4044
// Return the token.
4145
GetToken() string
4246
// Validate by token.
@@ -93,6 +97,11 @@ func (c *csrf) GetCookieHTTPOnly() bool {
9397
return c.CookieHTTPOnly
9498
}
9599

100+
// GetCookieDomain returns the flag value used for the csrf token.
101+
func (c *csrf) GetCookieDomain() string {
102+
return c.CookieDomain
103+
}
104+
96105
// GetToken returns the current token. This is typically used
97106
// to populate a hidden form in an HTML template.
98107
func (c *csrf) GetToken() string {
@@ -227,10 +236,14 @@ func Csrfer(opt CsrfOptions, ctx *Context) CSRF {
227236
if opt.CookieLifeTime == 0 {
228237
expires = time.Now().AddDate(0, 0, 1)
229238
}
230-
ctx.SetCookie(opt.Cookie, x.Token, opt.CookieLifeTime, opt.CookiePath, opt.CookieDomain, opt.Secure, opt.CookieHTTPOnly, expires,
231-
func(c *http.Cookie) {
232-
c.SameSite = opt.SameSite
233-
},
239+
middleware.SetCookie(ctx.Resp, opt.Cookie, x.Token,
240+
opt.CookieLifeTime,
241+
opt.CookiePath,
242+
opt.CookieDomain,
243+
opt.Secure,
244+
opt.CookieHTTPOnly,
245+
expires,
246+
middleware.SameSite(opt.SameSite),
234247
)
235248
}
236249
}
@@ -248,14 +261,22 @@ func Csrfer(opt CsrfOptions, ctx *Context) CSRF {
248261
func Validate(ctx *Context, x CSRF) {
249262
if token := ctx.Req.Header.Get(x.GetHeaderName()); len(token) > 0 {
250263
if !x.ValidToken(token) {
251-
ctx.SetCookie(x.GetCookieName(), "", -1, x.GetCookiePath())
264+
// Delete the cookie
265+
middleware.SetCookie(ctx.Resp, x.GetCookieName(), "",
266+
-1,
267+
x.GetCookiePath(),
268+
x.GetCookieDomain()) // FIXME: Do we need to set the Secure, httpOnly and SameSite values too?
252269
x.Error(ctx.Resp)
253270
}
254271
return
255272
}
256273
if token := ctx.Req.FormValue(x.GetFormName()); len(token) > 0 {
257274
if !x.ValidToken(token) {
258-
ctx.SetCookie(x.GetCookieName(), "", -1, x.GetCookiePath())
275+
// Delete the cookie
276+
middleware.SetCookie(ctx.Resp, x.GetCookieName(), "",
277+
-1,
278+
x.GetCookiePath(),
279+
x.GetCookieDomain()) // FIXME: Do we need to set the Secure, httpOnly and SameSite values too?
259280
x.Error(ctx.Resp)
260281
}
261282
return

modules/setting/session.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package setting
66

77
import (
8+
"net/http"
89
"path"
910
"path/filepath"
1011
"strings"
@@ -31,10 +32,13 @@ var (
3132
Secure bool
3233
// Cookie domain name. Default is empty.
3334
Domain string
35+
// SameSite declares if your cookie should be restricted to a first-party or same-site context. Valid strings are "none", "lax", "strict". Default is "lax"
36+
SameSite http.SameSite
3437
}{
3538
CookieName: "i_like_gitea",
3639
Gclifetime: 86400,
3740
Maxlifetime: 86400,
41+
SameSite: http.SameSiteLaxMode,
3842
}
3943
)
4044

@@ -52,6 +56,15 @@ func newSessionService() {
5256
SessionConfig.Gclifetime = sec.Key("GC_INTERVAL_TIME").MustInt64(86400)
5357
SessionConfig.Maxlifetime = sec.Key("SESSION_LIFE_TIME").MustInt64(86400)
5458
SessionConfig.Domain = sec.Key("DOMAIN").String()
59+
samesiteString := sec.Key("SAME_SITE").In("lax", []string{"none", "lax", "strict"})
60+
switch strings.ToLower(samesiteString) {
61+
case "none":
62+
SessionConfig.SameSite = http.SameSiteNoneMode
63+
case "strict":
64+
SessionConfig.SameSite = http.SameSiteStrictMode
65+
default:
66+
SessionConfig.SameSite = http.SameSiteLaxMode
67+
}
5568

5669
json := jsoniter.ConfigCompatibleWithStandardLibrary
5770
shadowConfig, err := json.Marshal(SessionConfig)

modules/web/middleware/cookie.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,47 @@ func NewCookie(name, value string, maxAge int) *http.Cookie {
7676
}
7777
}
7878

79+
// SetRedirectToCookie convenience function to set the RedirectTo cookie consistently
80+
func SetRedirectToCookie(resp http.ResponseWriter, value string) {
81+
SetCookie(resp, "redirect_to", value,
82+
0,
83+
setting.AppSubURL,
84+
"",
85+
setting.SessionConfig.Secure,
86+
true,
87+
SameSite(setting.SessionConfig.SameSite))
88+
}
89+
90+
// DeleteRedirectToCookie convenience function to delete most cookies consistently
91+
func DeleteRedirectToCookie(resp http.ResponseWriter) {
92+
SetCookie(resp, "redirect_to", "",
93+
-1,
94+
setting.AppSubURL,
95+
"",
96+
setting.SessionConfig.Secure,
97+
true,
98+
SameSite(setting.SessionConfig.SameSite))
99+
}
100+
101+
// DeleteSesionConfigPathCookie convenience function to delete SessionConfigPath cookies consistently
102+
func DeleteSesionConfigPathCookie(resp http.ResponseWriter, name string) {
103+
SetCookie(resp, name, "",
104+
-1,
105+
setting.SessionConfig.CookiePath,
106+
setting.SessionConfig.Domain,
107+
setting.SessionConfig.Secure,
108+
true,
109+
SameSite(setting.SessionConfig.SameSite))
110+
}
111+
112+
// DeleteCSRFCookie convenience function to delete SessionConfigPath cookies consistently
113+
func DeleteCSRFCookie(resp http.ResponseWriter) {
114+
SetCookie(resp, setting.CSRFCookieName, "",
115+
-1,
116+
setting.SessionConfig.CookiePath,
117+
setting.SessionConfig.Domain) // FIXME: Do we need to set the Secure, httpOnly and SameSite values too?
118+
}
119+
79120
// SetCookie set the cookies
80121
// TODO: Copied from gitea.com/macaron/macaron and should be improved after macaron removed.
81122
func SetCookie(resp http.ResponseWriter, name string, value string, others ...interface{}) {

modules/web/middleware/locale.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package middleware
77
import (
88
"net/http"
99

10+
"code.gitea.io/gitea/modules/setting"
1011
"code.gitea.io/gitea/modules/translation"
1112

1213
"github.com/unknwon/i18n"
@@ -42,8 +43,30 @@ func Locale(resp http.ResponseWriter, req *http.Request) translation.Locale {
4243
}
4344

4445
if changeLang {
45-
SetCookie(resp, "lang", lang, 1<<31-1)
46+
SetLocaleCookie(resp, lang, 1<<31-1)
4647
}
4748

4849
return translation.NewLocale(lang)
4950
}
51+
52+
// SetLocaleCookie convenience function to set the locale cookie consistently
53+
func SetLocaleCookie(resp http.ResponseWriter, lang string, expiry int) {
54+
SetCookie(resp, "lang", lang, expiry,
55+
setting.AppSubURL,
56+
setting.SessionConfig.Domain,
57+
setting.SessionConfig.Secure,
58+
true,
59+
SameSite(setting.SessionConfig.SameSite))
60+
}
61+
62+
// DeleteLocaleCookie convenience function to delete the locale cookie consistently
63+
// Setting the lang cookie will trigger the middleware to reset the language ot previous state.
64+
func DeleteLocaleCookie(resp http.ResponseWriter) {
65+
SetCookie(resp, "lang", "",
66+
-1,
67+
setting.AppSubURL,
68+
setting.SessionConfig.Domain,
69+
setting.SessionConfig.Secure,
70+
true,
71+
SameSite(setting.SessionConfig.SameSite))
72+
}

routers/home.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"code.gitea.io/gitea/modules/setting"
1818
"code.gitea.io/gitea/modules/structs"
1919
"code.gitea.io/gitea/modules/util"
20+
"code.gitea.io/gitea/modules/web/middleware"
2021
"code.gitea.io/gitea/routers/user"
2122
)
2223

@@ -46,7 +47,7 @@ func Home(ctx *context.Context) {
4647
} else if ctx.User.MustChangePassword {
4748
ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
4849
ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/change_password"
49-
ctx.SetCookie("redirect_to", setting.AppSubURL+ctx.Req.URL.RequestURI(), 0, setting.AppSubURL)
50+
middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI())
5051
ctx.Redirect(setting.AppSubURL + "/user/settings/change_password")
5152
} else {
5253
user.Dashboard(ctx)

routers/install.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -424,9 +424,10 @@ func InstallPost(ctx *context.Context) {
424424
}
425425

426426
days := 86400 * setting.LogInRememberDays
427-
ctx.SetCookie(setting.CookieUserName, u.Name, days, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true)
427+
ctx.SetCookie(setting.CookieUserName, u.Name, days)
428+
428429
ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands+u.Passwd),
429-
setting.CookieRememberName, u.Name, days, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true)
430+
setting.CookieRememberName, u.Name, days)
430431

431432
// Auto-login for admin
432433
if err = ctx.Session.Set("uid", u.ID); err != nil {

0 commit comments

Comments
 (0)