Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SameSite setting for cookies #14900

Merged
2 changes: 2 additions & 0 deletions custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -794,6 +794,8 @@ COOKIE_SECURE = false
GC_INTERVAL_TIME = 86400
; Session life time in seconds, default is 86400 (1 day)
SESSION_LIFE_TIME = 86400
; SameSite settings. Either "none", "lax", or "strict"
SAME_SITE=strict
zeripath marked this conversation as resolved.
Show resolved Hide resolved

[picture]
AVATAR_UPLOAD_PATH = data/avatars
Expand Down
2 changes: 2 additions & 0 deletions docs/content/doc/advanced/config-cheat-sheet.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,8 @@ Define allowed algorithms and their minimum key length (use -1 to disable a type
- `COOKIE_NAME`: **i\_like\_gitea**: The name of the cookie used for the session ID.
- `GC_INTERVAL_TIME`: **86400**: GC interval in seconds.
- `SESSION_LIFE_TIME`: **86400**: Session life time in seconds, default is 86400 (1 day)
- `DOMAIN`: **\<empty\>**: Sets the cookie Domain
zeripath marked this conversation as resolved.
Show resolved Hide resolved
- `SAME_SITE`: **strict** \[strict, lax, none\]: Set the SameSite setting for the cookie.
zeripath marked this conversation as resolved.
Show resolved Hide resolved

## Picture (`picture`)

Expand Down
5 changes: 2 additions & 3 deletions modules/auth/sso/sso.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware"
)

Expand Down Expand Up @@ -129,8 +128,8 @@ func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore
}
}

middleware.SetCookie(resp, "lang", user.Language, nil, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true)
middleware.SetLocaleCookie(resp, user.Language)

// Clear whatever CSRF has right now, force to generate a new one
middleware.SetCookie(resp, setting.CSRFCookieName, "", -1, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true)
middleware.DeleteCSRFCookie(resp)
}
7 changes: 4 additions & 3 deletions modules/context/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware"
)

// ToggleOptions contains required or check options
Expand Down Expand Up @@ -41,7 +42,7 @@ func Toggle(options *ToggleOptions) func(ctx *Context) {
ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/change_password"
if ctx.Req.URL.Path != "/user/events" {
ctx.SetCookie("redirect_to", setting.AppSubURL+ctx.Req.URL.RequestURI(), 0, setting.AppSubURL)
middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI())
}
ctx.Redirect(setting.AppSubURL + "/user/settings/change_password")
return
Expand Down Expand Up @@ -69,7 +70,7 @@ func Toggle(options *ToggleOptions) func(ctx *Context) {
if options.SignInRequired {
if !ctx.IsSigned {
if ctx.Req.URL.Path != "/user/events" {
ctx.SetCookie("redirect_to", setting.AppSubURL+ctx.Req.URL.RequestURI(), 0, setting.AppSubURL)
middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI())
}
ctx.Redirect(setting.AppSubURL + "/user/login")
return
Expand All @@ -84,7 +85,7 @@ func Toggle(options *ToggleOptions) func(ctx *Context) {
if !options.SignOutRequired && !ctx.IsSigned &&
len(ctx.GetCookie(setting.CookieUserName)) > 0 {
if ctx.Req.URL.Path != "/user/events" {
ctx.SetCookie("redirect_to", setting.AppSubURL+ctx.Req.URL.RequestURI(), 0, setting.AppSubURL)
middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI())
}
ctx.Redirect(setting.AppSubURL + "/user/login")
return
Expand Down
48 changes: 40 additions & 8 deletions modules/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,9 +386,28 @@ func (ctx *Context) Redirect(location string, status ...int) {
http.Redirect(ctx.Resp, ctx.Req, location, code)
}

// SetCookie set cookies to web browser
func (ctx *Context) SetCookie(name string, value string, others ...interface{}) {
middleware.SetCookie(ctx.Resp, name, value, others...)
// SetCookie convenience function to set most cookies consistently
// CSRF and a few others are the exception here
func (ctx *Context) SetCookie(name, value string, expiry int) {
middleware.SetCookie(ctx.Resp, name, value,
expiry,
setting.AppSubURL,
setting.SessionConfig.Domain,
setting.SessionConfig.Secure,
true,
middleware.SameSite(setting.SessionConfig.SameSite))
}

// DeleteCookie convenience function to delete most cookies consistently
// CSRF and a few others are the exception here
func (ctx *Context) DeleteCookie(name string) {
middleware.SetCookie(ctx.Resp, name, "",
-1,
setting.AppSubURL,
setting.SessionConfig.Domain,
setting.SessionConfig.Secure,
true,
middleware.SameSite(setting.SessionConfig.SameSite))
}

// GetCookie returns given cookie value from request header.
Expand All @@ -399,6 +418,11 @@ func (ctx *Context) GetCookie(name string) string {
// GetSuperSecureCookie returns given cookie value from request header with secret string.
func (ctx *Context) GetSuperSecureCookie(secret, name string) (string, bool) {
val := ctx.GetCookie(name)
return ctx.CookieDecrypt(secret, val)
}

// CookieDecrypt returns given value from with secret string.
func (ctx *Context) CookieDecrypt(secret, val string) (string, bool) {
if val == "" {
return "", false
}
Expand All @@ -414,14 +438,21 @@ func (ctx *Context) GetSuperSecureCookie(secret, name string) (string, bool) {
}

// SetSuperSecureCookie sets given cookie value to response header with secret string.
func (ctx *Context) SetSuperSecureCookie(secret, name, value string, others ...interface{}) {
func (ctx *Context) SetSuperSecureCookie(secret, name, value string, expiry int) {
text := ctx.CookieEncrypt(secret, value)

ctx.SetCookie(name, text, expiry)
}

// CookieEncrypt encrypts a given value using the provided secret
func (ctx *Context) CookieEncrypt(secret, value string) string {
key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New)
text, err := com.AESGCMEncrypt(key, []byte(value))
if err != nil {
panic("error encrypting cookie: " + err.Error())
}

ctx.SetCookie(name, hex.EncodeToString(text), others...)
return hex.EncodeToString(text)
}

// GetCookieInt returns cookie result in int type.
Expand Down Expand Up @@ -533,6 +564,7 @@ func getCsrfOpts() CsrfOptions {
Header: "X-Csrf-Token",
CookieDomain: setting.SessionConfig.Domain,
CookiePath: setting.SessionConfig.CookiePath,
SameSite: setting.SessionConfig.SameSite,
}
}

Expand Down Expand Up @@ -597,17 +629,17 @@ func Contexter() func(next http.Handler) http.Handler {
middleware.Domain(setting.SessionConfig.Domain),
middleware.HTTPOnly(true),
middleware.Secure(setting.SessionConfig.Secure),
//middlewares.SameSite(opt.SameSite), FIXME: we need a samesite config
middleware.SameSite(setting.SessionConfig.SameSite),
)
return
}

ctx.SetCookie("macaron_flash", "", -1,
middleware.SetCookie(ctx.Resp, "macaron_flash", "", -1,
setting.SessionConfig.CookiePath,
middleware.Domain(setting.SessionConfig.Domain),
middleware.HTTPOnly(true),
middleware.Secure(setting.SessionConfig.Secure),
//middleware.SameSite(), FIXME: we need a samesite config
middleware.SameSite(setting.SessionConfig.SameSite),
)
})

Expand Down
33 changes: 27 additions & 6 deletions modules/context/csrf.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
"net/http"
"time"

"code.gitea.io/gitea/modules/web/middleware"

"github.com/unknwon/com"
)

Expand All @@ -37,6 +39,8 @@ type CSRF interface {
GetCookiePath() string
// Return the flag value used for the csrf token.
GetCookieHTTPOnly() bool
// Return cookie domain
GetCookieDomain() string
// Return the token.
GetToken() string
// Validate by token.
Expand Down Expand Up @@ -93,6 +97,11 @@ func (c *csrf) GetCookieHTTPOnly() bool {
return c.CookieHTTPOnly
}

// GetCookieDomain returns the flag value used for the csrf token.
func (c *csrf) GetCookieDomain() string {
return c.CookieDomain
}

// GetToken returns the current token. This is typically used
// to populate a hidden form in an HTML template.
func (c *csrf) GetToken() string {
Expand Down Expand Up @@ -227,10 +236,14 @@ func Csrfer(opt CsrfOptions, ctx *Context) CSRF {
if opt.CookieLifeTime == 0 {
expires = time.Now().AddDate(0, 0, 1)
}
ctx.SetCookie(opt.Cookie, x.Token, opt.CookieLifeTime, opt.CookiePath, opt.CookieDomain, opt.Secure, opt.CookieHTTPOnly, expires,
func(c *http.Cookie) {
c.SameSite = opt.SameSite
},
middleware.SetCookie(ctx.Resp, opt.Cookie, x.Token,
opt.CookieLifeTime,
opt.CookiePath,
opt.CookieDomain,
opt.Secure,
opt.CookieHTTPOnly,
expires,
middleware.SameSite(opt.SameSite),
)
}
}
Expand All @@ -248,14 +261,22 @@ func Csrfer(opt CsrfOptions, ctx *Context) CSRF {
func Validate(ctx *Context, x CSRF) {
if token := ctx.Req.Header.Get(x.GetHeaderName()); len(token) > 0 {
if !x.ValidToken(token) {
ctx.SetCookie(x.GetCookieName(), "", -1, x.GetCookiePath())
// Delete the cookie
middleware.SetCookie(ctx.Resp, x.GetCookieName(), "",
-1,
x.GetCookiePath(),
x.GetCookieDomain()) // FIXME: Do we need to set the Secure, httpOnly and SameSite values too?
x.Error(ctx.Resp)
}
return
}
if token := ctx.Req.FormValue(x.GetFormName()); len(token) > 0 {
if !x.ValidToken(token) {
ctx.SetCookie(x.GetCookieName(), "", -1, x.GetCookiePath())
// Delete the cookie
middleware.SetCookie(ctx.Resp, x.GetCookieName(), "",
-1,
x.GetCookiePath(),
x.GetCookieDomain()) // FIXME: Do we need to set the Secure, httpOnly and SameSite values too?
x.Error(ctx.Resp)
}
return
Expand Down
13 changes: 13 additions & 0 deletions modules/setting/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package setting

import (
"net/http"
"path"
"path/filepath"
"strings"
Expand All @@ -31,10 +32,13 @@ var (
Secure bool
// Cookie domain name. Default is empty.
Domain string
// SameSite declares if your cookie should be restricted to a first-party or same-site context. Valid strings are "none", "lax", "strict". Default is "strict"
zeripath marked this conversation as resolved.
Show resolved Hide resolved
SameSite http.SameSite
}{
CookieName: "i_like_gitea",
Gclifetime: 86400,
Maxlifetime: 86400,
SameSite: http.SameSiteStrictMode,
zeripath marked this conversation as resolved.
Show resolved Hide resolved
}
)

Expand All @@ -52,6 +56,15 @@ func newSessionService() {
SessionConfig.Gclifetime = sec.Key("GC_INTERVAL_TIME").MustInt64(86400)
SessionConfig.Maxlifetime = sec.Key("SESSION_LIFE_TIME").MustInt64(86400)
SessionConfig.Domain = sec.Key("DOMAIN").String()
samesiteString := sec.Key("SAME_SITE").In("strict", []string{"none", "lax", "strict"})
zeripath marked this conversation as resolved.
Show resolved Hide resolved
switch strings.ToLower(samesiteString) {
case "none":
SessionConfig.SameSite = http.SameSiteNoneMode
case "lax":
zeripath marked this conversation as resolved.
Show resolved Hide resolved
SessionConfig.SameSite = http.SameSiteLaxMode
zeripath marked this conversation as resolved.
Show resolved Hide resolved
default:
SessionConfig.SameSite = http.SameSiteStrictMode
zeripath marked this conversation as resolved.
Show resolved Hide resolved
}

json := jsoniter.ConfigCompatibleWithStandardLibrary
shadowConfig, err := json.Marshal(SessionConfig)
Expand Down
41 changes: 41 additions & 0 deletions modules/web/middleware/cookie.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,47 @@ func NewCookie(name, value string, maxAge int) *http.Cookie {
}
}

// SetRedirectToCookie convenience function to set the RedirectTo cookie consistently
func SetRedirectToCookie(resp http.ResponseWriter, value string) {
SetCookie(resp, "redirect_to", value,
0,
setting.AppSubURL,
"",
setting.SessionConfig.Secure,
true,
SameSite(setting.SessionConfig.SameSite))
}

// DeleteRedirectToCookie convenience function to delete most cookies consistently
func DeleteRedirectToCookie(resp http.ResponseWriter) {
SetCookie(resp, "redirect_to", "",
-1,
setting.AppSubURL,
"",
setting.SessionConfig.Secure,
true,
SameSite(setting.SessionConfig.SameSite))
}

// DeleteSesionConfigPathCookie convenience function to delete SessionConfigPath cookies consistently
func DeleteSesionConfigPathCookie(resp http.ResponseWriter, name string) {
SetCookie(resp, name, "",
-1,
setting.SessionConfig.CookiePath,
setting.SessionConfig.Domain,
setting.SessionConfig.Secure,
true,
SameSite(setting.SessionConfig.SameSite))
}

// DeleteCSRFCookie convenience function to delete SessionConfigPath cookies consistently
func DeleteCSRFCookie(resp http.ResponseWriter) {
SetCookie(resp, setting.CSRFCookieName, "",
-1,
setting.SessionConfig.CookiePath,
setting.SessionConfig.Domain) // FIXME: Do we need to set the Secure, httpOnly and SameSite values too?
}

// SetCookie set the cookies
// TODO: Copied from gitea.com/macaron/macaron and should be improved after macaron removed.
func SetCookie(resp http.ResponseWriter, name string, value string, others ...interface{}) {
Expand Down
25 changes: 24 additions & 1 deletion modules/web/middleware/locale.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package middleware
import (
"net/http"

"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/translation"

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

if changeLang {
SetCookie(resp, "lang", lang, 1<<31-1)
SetLocaleCookie(resp, lang)
zeripath marked this conversation as resolved.
Show resolved Hide resolved
}

return translation.NewLocale(lang)
}

// SetLocaleCookie convenience function to set the locale cookie consistently
func SetLocaleCookie(resp http.ResponseWriter, lang string) {
SetCookie(resp, "lang", lang, 0,
setting.AppSubURL,
setting.SessionConfig.Domain,
setting.SessionConfig.Secure,
true,
SameSite(setting.SessionConfig.SameSite))
}

// DeleteLocaleCookie convenience function to delete the locale cookie consistently
// Setting the lang cookie will trigger the middleware to reset the language ot previous state.
func DeleteLocaleCookie(resp http.ResponseWriter) {
SetCookie(resp, "lang", "",
-1,
setting.AppSubURL,
setting.SessionConfig.Domain,
setting.SessionConfig.Secure,
true,
SameSite(setting.SessionConfig.SameSite))
}
3 changes: 2 additions & 1 deletion routers/home.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/routers/user"
)

Expand Down Expand Up @@ -46,7 +47,7 @@ func Home(ctx *context.Context) {
} else if ctx.User.MustChangePassword {
ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/change_password"
ctx.SetCookie("redirect_to", setting.AppSubURL+ctx.Req.URL.RequestURI(), 0, setting.AppSubURL)
middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI())
ctx.Redirect(setting.AppSubURL + "/user/settings/change_password")
} else {
user.Dashboard(ctx)
Expand Down
5 changes: 3 additions & 2 deletions routers/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -424,9 +424,10 @@ func InstallPost(ctx *context.Context) {
}

days := 86400 * setting.LogInRememberDays
ctx.SetCookie(setting.CookieUserName, u.Name, days, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true)
ctx.SetCookie(setting.CookieUserName, u.Name, days)

ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands+u.Passwd),
setting.CookieRememberName, u.Name, days, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true)
setting.CookieRememberName, u.Name, days)

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