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

Refactor CSRF protection modules, make sure CSRF tokens can be up-to-date. #19337

Merged
merged 11 commits into from
Apr 8, 2022
7 changes: 3 additions & 4 deletions modules/context/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ package context
import (
"context"
"fmt"
"html"
"net/http"
"net/url"
"strings"
Expand Down Expand Up @@ -196,7 +195,7 @@ func (ctx *APIContext) RequireCSRF() {
headerToken := ctx.Req.Header.Get(ctx.csrf.GetHeaderName())
formValueToken := ctx.Req.FormValue(ctx.csrf.GetFormName())
if len(headerToken) > 0 || len(formValueToken) > 0 {
Validate(ctx.Context, ctx.csrf)
ctx.csrf.Validate(ctx.Context)
} else {
ctx.Context.Error(http.StatusUnauthorized, "Missing CSRF token.")
}
Expand Down Expand Up @@ -273,7 +272,7 @@ func APIContexter() func(http.Handler) http.Handler {
}

ctx.Req = WithAPIContext(WithContext(req, ctx.Context), &ctx)
ctx.csrf = Csrfer(csrfOpts, ctx.Context)
ctx.csrf = PrepareCSRFProtector(csrfOpts, ctx.Context)

// If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid.
if ctx.Req.Method == "POST" && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") {
Expand All @@ -285,7 +284,7 @@ func APIContexter() func(http.Handler) http.Handler {

ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)

ctx.Data["CsrfToken"] = html.EscapeString(ctx.csrf.GetToken())
ctx.Data["CsrfToken"] = ctx.csrf.GetToken()
ctx.Data["Context"] = &ctx

next.ServeHTTP(ctx.Resp, ctx.Req)
Expand Down
2 changes: 1 addition & 1 deletion modules/context/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func Toggle(options *ToggleOptions) func(ctx *Context) {
}

if !options.SignOutRequired && !options.DisableCSRF && ctx.Req.Method == "POST" {
Validate(ctx, ctx.csrf)
ctx.csrf.Validate(ctx)
if ctx.Written() {
return
}
Expand Down
10 changes: 6 additions & 4 deletions modules/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ type Context struct {
Render Render
translation.Locale
Cache cache.Cache
csrf CSRF
csrf CSRFProtector
Flash *middleware.Flash
Session session.Store

Expand Down Expand Up @@ -666,7 +666,9 @@ func Auth(authMethod auth.Method) func(*Context) {
func Contexter() func(next http.Handler) http.Handler {
rnd := templates.HTMLRenderer()
csrfOpts := getCsrfOpts()

if !setting.IsProd {
CsrfTokenRegenerationInterval = 5 * time.Second // in dev, re-generate the tokens more aggressively for debug purpose
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
locale := middleware.Locale(resp, req)
Expand Down Expand Up @@ -697,7 +699,7 @@ func Contexter() func(next http.Handler) http.Handler {
ctx.Data["Context"] = &ctx

ctx.Req = WithContext(req, &ctx)
ctx.csrf = Csrfer(csrfOpts, &ctx)
ctx.csrf = PrepareCSRFProtector(csrfOpts, &ctx)

// Get flash.
flashCookie := ctx.GetCookie("macaron_flash")
Expand Down Expand Up @@ -755,7 +757,7 @@ func Contexter() func(next http.Handler) http.Handler {

ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)

ctx.Data["CsrfToken"] = html.EscapeString(ctx.csrf.GetToken())
ctx.Data["CsrfToken"] = ctx.csrf.GetToken()
ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + ctx.Data["CsrfToken"].(string) + `">`)

// FIXME: do we really always need these setting? There should be someway to have to avoid having to always set these
Expand Down
187 changes: 58 additions & 129 deletions modules/context/csrf.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,29 +31,19 @@ import (
"code.gitea.io/gitea/modules/web/middleware"
)

// CSRF represents a CSRF service and is used to get the current token and validate a suspect token.
type CSRF interface {
// Return HTTP header to search for token.
// CSRFProtector represents a CSRF protector and is used to get the current token and validate the token.
type CSRFProtector interface {
// GetHeaderName returns HTTP header to search for token.
GetHeaderName() string
// Return form value to search for token.
// GetFormName returns form value to search for token.
GetFormName() string
// Return cookie name to search for token.
GetCookieName() string
// Return cookie path
GetCookiePath() string
// Return the flag value used for the csrf token.
GetCookieHTTPOnly() bool
// Return cookie domain
GetCookieDomain() string
// Return the token.
// GetToken returns the token.
GetToken() string
// Validate by token.
ValidToken(t string) bool
// Error replies to the request with a custom function when ValidToken fails.
Error(w http.ResponseWriter)
// Validate validates the token in http context.
Validate(ctx *Context)
}

type csrf struct {
type csrfProtector struct {
// Header name value for setting and getting csrf token.
Header string
// Form name value for setting and getting csrf token.
Expand All @@ -72,56 +62,24 @@ type csrf struct {
ID string
// Secret used along with the unique id above to generate the Token.
Secret string
// ErrorFunc is the custom function that replies to the request when ValidToken fails.
ErrorFunc func(w http.ResponseWriter)
}

// GetHeaderName returns the name of the HTTP header for csrf token.
func (c *csrf) GetHeaderName() string {
func (c *csrfProtector) GetHeaderName() string {
return c.Header
}

// GetFormName returns the name of the form value for csrf token.
func (c *csrf) GetFormName() string {
func (c *csrfProtector) GetFormName() string {
return c.Form
}

// GetCookieName returns the name of the cookie for csrf token.
func (c *csrf) GetCookieName() string {
return c.Cookie
}

// GetCookiePath returns the path of the cookie for csrf token.
func (c *csrf) GetCookiePath() string {
return c.CookiePath
}

// GetCookieHTTPOnly returns the flag value used for the csrf token.
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 {
func (c *csrfProtector) GetToken() string {
return c.Token
}

// ValidToken validates the passed token against the existing Secret and ID.
func (c *csrf) ValidToken(t string) bool {
return ValidToken(t, c.Secret, c.ID, "POST")
}

// Error replies to the request when ValidToken fails.
func (c *csrf) Error(w http.ResponseWriter) {
c.ErrorFunc(w)
}

// CsrfOptions maintains options to manage behavior of Generate.
type CsrfOptions struct {
// The global secret value used to generate Tokens.
Expand All @@ -143,73 +101,58 @@ type CsrfOptions struct {
SessionKey string
// oldSessionKey saves old value corresponding to SessionKey.
oldSessionKey string
// If true, send token via X-CSRFToken header.
// If true, send token via X-Csrf-Token header.
SetHeader bool
// If true, send token via _csrf cookie.
SetCookie bool
// Set the Secure flag to true on the cookie.
Secure bool
// Disallow Origin appear in request header.
Origin bool
// The function called when Validate fails.
ErrorFunc func(w http.ResponseWriter)
// Cookie life time. Default is 0
// Cookie lifetime. Default is 0
CookieLifeTime int
}

func prepareOptions(options []CsrfOptions) CsrfOptions {
var opt CsrfOptions
if len(options) > 0 {
opt = options[0]
}

// Defaults.
if len(opt.Secret) == 0 {
func prepareDefaultCsrfOptions(opt CsrfOptions) CsrfOptions {
if opt.Secret == "" {
randBytes, err := util.CryptoRandomBytes(8)
if err != nil {
// this panic can be handled by the recover() in http handlers
panic(fmt.Errorf("failed to generate random bytes: %w", err))
}
opt.Secret = base32.StdEncoding.EncodeToString(randBytes)
}
if len(opt.Header) == 0 {
opt.Header = "X-CSRFToken"
if opt.Header == "" {
opt.Header = "X-Csrf-Token"
}
if len(opt.Form) == 0 {
if opt.Form == "" {
opt.Form = "_csrf"
}
if len(opt.Cookie) == 0 {
if opt.Cookie == "" {
opt.Cookie = "_csrf"
}
if len(opt.CookiePath) == 0 {
if opt.CookiePath == "" {
opt.CookiePath = "/"
}
if len(opt.SessionKey) == 0 {
if opt.SessionKey == "" {
opt.SessionKey = "uid"
}
opt.oldSessionKey = "_old_" + opt.SessionKey
if opt.ErrorFunc == nil {
opt.ErrorFunc = func(w http.ResponseWriter) {
http.Error(w, "Invalid csrf token.", http.StatusBadRequest)
}
}

return opt
}

// Csrfer maps CSRF to each request. If this request is a Get request, it will generate a new token.
// PrepareCSRFProtector returns a CSRFProtector to be used for every request.
// Additionally, depending on options set, generated tokens will be sent via Header and/or Cookie.
func Csrfer(opt CsrfOptions, ctx *Context) CSRF {
opt = prepareOptions([]CsrfOptions{opt})
x := &csrf{
func PrepareCSRFProtector(opt CsrfOptions, ctx *Context) CSRFProtector {
opt = prepareDefaultCsrfOptions(opt)
x := &csrfProtector{
Secret: opt.Secret,
Header: opt.Header,
Form: opt.Form,
Cookie: opt.Cookie,
CookieDomain: opt.CookieDomain,
CookiePath: opt.CookiePath,
CookieHTTPOnly: opt.CookieHTTPOnly,
ErrorFunc: opt.ErrorFunc,
}

if opt.Origin && len(ctx.Req.Header.Get("Origin")) > 0 {
Expand All @@ -229,28 +172,31 @@ func Csrfer(opt CsrfOptions, ctx *Context) CSRF {
}
}

needsNew := false
oldUID := ctx.Session.Get(opt.oldSessionKey)
if oldUID == nil || oldUID.(string) != x.ID {
needsNew = true
uidChanged := oldUID == nil || oldUID.(string) != x.ID
cookieToken := ctx.GetCookie(opt.Cookie)

needsNew := true
if uidChanged {
_ = ctx.Session.Set(opt.oldSessionKey, x.ID)
} else {
// If cookie present, map existing token, else generate a new one.
if val := ctx.GetCookie(opt.Cookie); len(val) > 0 {
// FIXME: test coverage.
x.Token = val
} else {
needsNew = true
} else if cookieToken != "" {
// If cookie token presents, re-use existing unexpired token, else generate a new one.
if issueTime, ok := ParseCsrfToken(cookieToken); ok {
dur := time.Since(issueTime) // issueTime is not a monotonic-clock, the server time may change a lot to an early time.
if dur >= -CsrfTokenRegenerationInterval && dur <= CsrfTokenRegenerationInterval {
x.Token = cookieToken
needsNew = false
}
}
}

if needsNew {
// FIXME: actionId.
x.Token = GenerateToken(x.Secret, x.ID, "POST")
x.Token = GenerateCsrfToken(x.Secret, x.ID, "POST", time.Now())
if opt.SetCookie {
var expires interface{}
if opt.CookieLifeTime == 0 {
expires = time.Now().AddDate(0, 0, 1)
expires = time.Now().Add(CsrfTokenTimeout)
}
middleware.SetCookie(ctx.Resp, opt.Cookie, x.Token,
opt.CookieLifeTime,
Expand All @@ -270,47 +216,30 @@ func Csrfer(opt CsrfOptions, ctx *Context) CSRF {
return x
}

// Validate should be used as a per route middleware. It attempts to get a token from a "X-CSRFToken"
// HTTP header and then a "_csrf" form value. If one of these is found, the token will be validated
// using ValidToken. If this validation fails, custom Error is sent in the reply.
// If neither a header or form value is found, http.StatusBadRequest is sent.
func Validate(ctx *Context, x CSRF) {
if token := ctx.Req.Header.Get(x.GetHeaderName()); len(token) > 0 {
if !x.ValidToken(token) {
// 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?
if middleware.IsAPIPath(ctx.Req) {
x.Error(ctx.Resp)
return
}
func (c *csrfProtector) validateToken(ctx *Context, token string) {
if !ValidCsrfToken(token, c.Secret, c.ID, "POST", time.Now()) {
middleware.DeleteCSRFCookie(ctx.Resp)
if middleware.IsAPIPath(ctx.Req) {
http.Error(ctx.Resp, "Invalid CSRF token.", http.StatusBadRequest)
} else {
ctx.Flash.Error(ctx.Tr("error.invalid_csrf"))
ctx.Redirect(setting.AppSubURL + "/")
}
return
}
if token := ctx.Req.FormValue(x.GetFormName()); len(token) > 0 {
if !x.ValidToken(token) {
// 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?
if middleware.IsAPIPath(ctx.Req) {
x.Error(ctx.Resp)
return
}
ctx.Flash.Error(ctx.Tr("error.invalid_csrf"))
ctx.Redirect(setting.AppSubURL + "/")
}
}

// Validate should be used as a per route middleware. It attempts to get a token from an "X-Csrf-Token"
// HTTP header and then a "_csrf" form value. If one of these is found, the token will be validated.
// If this validation fails, custom Error is sent in the reply.
// If neither a header nor form value is found, http.StatusBadRequest is sent.
func (c *csrfProtector) Validate(ctx *Context) {
if token := ctx.Req.Header.Get(c.GetHeaderName()); token != "" {
c.validateToken(ctx, token)
return
}
if middleware.IsAPIPath(ctx.Req) {
http.Error(ctx.Resp, "Bad Request: no CSRF token present", http.StatusBadRequest)
if token := ctx.Req.FormValue(c.GetFormName()); token != "" {
c.validateToken(ctx, token)
return
}
ctx.Flash.Error(ctx.Tr("error.missing_csrf"))
ctx.Redirect(setting.AppSubURL + "/")
c.validateToken(ctx, "") // no csrf token, use an empty token to respond error
}
Loading