diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 475c62ab6c423..40c066c2b1054 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -503,9 +503,6 @@ INTERNAL_TOKEN = ;; Password Hash algorithm, either "argon2", "pbkdf2", "scrypt" or "bcrypt" ;PASSWORD_HASH_ALGO = pbkdf2 ;; -;; Set false to allow JavaScript to read CSRF cookie -;CSRF_COOKIE_HTTP_ONLY = true -;; ;; Validate against https://haveibeenpwned.com/Passwords to see if a password has been exposed ;PASSWORD_CHECK_PWN = false ;; diff --git a/modules/setting/markup.go b/modules/setting/markup.go index e105506fc068c..caf0d5f8d94b6 100644 --- a/modules/setting/markup.go +++ b/modules/setting/markup.go @@ -255,7 +255,7 @@ func newMarkupRenderer(name string, sec ConfigSection) { } // ATTENTION! at the moment, only a safe set like "allow-scripts" are allowed for sandbox mode. - // "allow-same-origin" should never be used, it leads to XSS attack, and it makes the JS in iframe can access parent window's config and CSRF token + // "allow-same-origin" should NEVER be used, it leads to XSS attack: makes the JS in iframe can access parent window's config and send requests with user's credentials. renderContentSandbox := sec.Key("RENDER_CONTENT_SANDBOX").MustString("allow-scripts allow-popups") if renderContentSandbox == "disabled" { renderContentSandbox = "" diff --git a/modules/setting/oauth2.go b/modules/setting/oauth2.go index ae2a9d7bee71c..2dfe77dda9a9c 100644 --- a/modules/setting/oauth2.go +++ b/modules/setting/oauth2.go @@ -133,7 +133,7 @@ func loadOAuth2From(rootCfg ConfigProvider) { // FIXME: at the moment, no matter oauth2 is enabled or not, it must generate a "oauth2 JWT_SECRET" // Because this secret is also used as GeneralTokenSigningSecret (as a quick not-that-breaking fix for some legacy problems). - // Including: CSRF token, account validation token, etc ... + // Including: account validation token, etc ... // In main branch, the signing token should be refactored (eg: one unique for LFS/OAuth2/etc ...) jwtSecretBase64 := loadSecret(sec, "JWT_SECRET_URI", "JWT_SECRET") if InstallLock { diff --git a/modules/setting/security.go b/modules/setting/security.go index 153b6bc944ff5..d60cfbbfc8690 100644 --- a/modules/setting/security.go +++ b/modules/setting/security.go @@ -36,8 +36,6 @@ var ( PasswordCheckPwn bool SuccessfulTokensCacheSize int DisableQueryAuthToken bool - CSRFCookieName = "_csrf" - CSRFCookieHTTPOnly = true RecordUserSignupMetadata = false TwoFactorAuthEnforced = false ) @@ -139,7 +137,6 @@ func loadSecurityFrom(rootCfg ConfigProvider) { log.Fatal("The provided password hash algorithm was invalid: %s", sec.Key("PASSWORD_HASH_ALGO").MustString("")) } - CSRFCookieHTTPOnly = sec.Key("CSRF_COOKIE_HTTP_ONLY").MustBool(true) PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false) SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20) diff --git a/routers/common/auth.go b/routers/common/auth.go index 115d65ed104a8..80307906ba550 100644 --- a/routers/common/auth.go +++ b/routers/common/auth.go @@ -41,5 +41,4 @@ type VerifyOptions struct { SignInRequired bool SignOutRequired bool AdminRequired bool - DisableCSRF bool } diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 2ccd1c71b5ce3..d36fb5bab75cd 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -102,7 +102,6 @@ func autoSignIn(ctx *context.Context) (bool, error) { return false, err } - ctx.Csrf.PrepareForSessionUser(ctx) return true, nil } @@ -357,9 +356,6 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req) } - // force to generate a new CSRF token - ctx.Csrf.PrepareForSessionUser(ctx) - // Register last login if err := user_service.UpdateUser(ctx, u, &user_service.UpdateOptions{SetLastLogin: true}); err != nil { ctx.ServerError("UpdateUser", err) @@ -403,7 +399,6 @@ func HandleSignOut(ctx *context.Context) { _ = ctx.Session.Flush() _ = ctx.Session.Destroy(ctx.Resp, ctx.Req) ctx.DeleteSiteCookie(setting.CookieRememberName) - ctx.Csrf.DeleteCookie(ctx) middleware.DeleteRedirectToCookie(ctx.Resp) } @@ -811,8 +806,6 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) { return } - ctx.Csrf.PrepareForSessionUser(ctx) - if err := resetLocale(ctx, user); err != nil { ctx.ServerError("resetLocale", err) return diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index f7ce5875ca873..5eab7ffeb449d 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -393,9 +393,6 @@ func handleOAuth2SignIn(ctx *context.Context, authSource *auth.Source, u *user_m return } - // force to generate a new CSRF token - ctx.Csrf.PrepareForSessionUser(ctx) - if err := resetLocale(ctx, u); err != nil { ctx.ServerError("resetLocale", err) return diff --git a/routers/web/githttp.go b/routers/web/githttp.go index 06de811f16e11..4a4fb0b79fad4 100644 --- a/routers/web/githttp.go +++ b/routers/web/githttp.go @@ -22,5 +22,5 @@ func addOwnerRepoGitHTTPRouters(m *web.Router) { m.Methods("GET,OPTIONS", "/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38,62}}", repo.GetLooseObject) m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.pack", repo.GetPackFile) m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.idx", repo.GetIdxFile) - }, optSignInIgnoreCsrf, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb()) + }, optSignIn, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb()) } diff --git a/routers/web/user/setting/keys.go b/routers/web/user/setting/keys.go index 6b5a7a2e2a0bd..86eab730b1df8 100644 --- a/routers/web/user/setting/keys.go +++ b/routers/web/user/setting/keys.go @@ -325,7 +325,7 @@ func loadKeysData(ctx *context.Context) { ctx.Data["GPGKeys"] = gpgkeys tokenToSign := asymkey_model.VerificationToken(ctx.Doer, 1) - // generate a new aes cipher using the csrfToken + // generate a new aes cipher using the token ctx.Data["TokenToSign"] = tokenToSign principals, err := db.Find[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{ diff --git a/routers/web/web.go b/routers/web/web.go index 86e51d607e2fd..ec27439c9b31d 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -129,13 +129,13 @@ func webAuth(authMethod auth_service.Method) func(*context.Context) { // ensure the session uid is deleted _ = ctx.Session.Delete("uid") } - - ctx.Csrf.PrepareForSessionUser(ctx) } } // verifyAuthWithOptions checks authentication according to options func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.Context) { + crossOrginProtection := http.NewCrossOriginProtection() + return func(ctx *context.Context) { // Check prohibit login users. if ctx.IsSigned { @@ -178,9 +178,9 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.Cont return } - if !options.SignOutRequired && !options.DisableCSRF && ctx.Req.Method == http.MethodPost { - ctx.Csrf.Validate(ctx) - if ctx.Written() { + if !options.SignOutRequired { + if err := crossOrginProtection.Check(ctx.Req); err != nil { + http.Error(ctx.Resp, err.Error(), http.StatusForbidden) return } } @@ -292,17 +292,20 @@ func Routes() *web.Router { return routes } -var optSignInIgnoreCsrf = verifyAuthWithOptions(&common.VerifyOptions{DisableCSRF: true}) +// required to be signed in or signed out +var ( + reqSignIn = verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true}) + reqSignOut = verifyAuthWithOptions(&common.VerifyOptions{SignOutRequired: true}) +) + +// optional sign in (if signed in, use the user as doer, if not, no doer) +var ( + optSignIn = verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict}) + optExploreSignIn = verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict || setting.Service.Explore.RequireSigninView}) +) // registerWebRoutes register routes func registerWebRoutes(m *web.Router) { - // required to be signed in or signed out - reqSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true}) - reqSignOut := verifyAuthWithOptions(&common.VerifyOptions{SignOutRequired: true}) - // optional sign in (if signed in, use the user as doer, if not, no doer) - optSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict}) - optExploreSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict || setting.Service.Explore.RequireSigninView}) - validation.AddBindingRules() openIDSignInEnabled := func(ctx *context.Context) { @@ -489,7 +492,7 @@ func registerWebRoutes(m *web.Router) { m.Post("/-/markup", reqSignIn, web.Bind(structs.MarkupOption{}), misc.Markup) m.Get("/-/web-theme/list", misc.WebThemeList) - m.Post("/-/web-theme/apply", optSignInIgnoreCsrf, misc.WebThemeApply) + m.Post("/-/web-theme/apply", misc.WebThemeApply) m.Group("/explore", func() { m.Get("", func(ctx *context.Context) { @@ -565,12 +568,14 @@ func registerWebRoutes(m *web.Router) { m.Post("/grant", web.Bind(forms.GrantApplicationForm{}), auth.GrantApplicationOAuth) // TODO manage redirection m.Post("/authorize", web.Bind(forms.AuthorizationForm{}), auth.AuthorizeOAuth) - }, optSignInIgnoreCsrf, reqSignIn) + }, reqSignIn) - m.Methods("GET, POST, OPTIONS", "/userinfo", optionsCorsHandler(), optSignInIgnoreCsrf, auth.InfoOAuth) - m.Methods("POST, OPTIONS", "/access_token", optionsCorsHandler(), web.Bind(forms.AccessTokenForm{}), optSignInIgnoreCsrf, auth.AccessTokenOAuth) - m.Methods("GET, OPTIONS", "/keys", optionsCorsHandler(), optSignInIgnoreCsrf, auth.OIDCKeys) - m.Methods("POST, OPTIONS", "/introspect", optionsCorsHandler(), web.Bind(forms.IntrospectTokenForm{}), optSignInIgnoreCsrf, auth.IntrospectOAuth) + m.Group("", func() { + m.Methods("GET, POST, OPTIONS", "/userinfo", auth.InfoOAuth) + m.Methods("POST, OPTIONS", "/access_token", web.Bind(forms.AccessTokenForm{}), auth.AccessTokenOAuth) + m.Methods("GET, OPTIONS", "/keys", auth.OIDCKeys) + m.Methods("POST, OPTIONS", "/introspect", web.Bind(forms.IntrospectTokenForm{}), auth.IntrospectOAuth) + }, optSignIn, optionsCorsHandler()) }, oauth2Enabled) m.Group("/user/settings", func() { @@ -1653,7 +1658,7 @@ func registerWebRoutes(m *web.Router) { m.Post("/action/{action:accept_transfer|reject_transfer}", reqSignIn, repo.ActionTransfer) }, optSignIn, context.RepoAssignment) - common.AddOwnerRepoGitLFSRoutes(m, optSignInIgnoreCsrf, lfsServerEnabled) // "/{username}/{reponame}/{lfs-paths}": git-lfs support + common.AddOwnerRepoGitLFSRoutes(m, optSignIn, lfsServerEnabled) // "/{username}/{reponame}/{lfs-paths}": git-lfs support addOwnerRepoGitHTTPRouters(m) // "/{username}/{reponame}/{git-paths}": git http support diff --git a/services/auth/auth.go b/services/auth/auth.go index 291e78a7358a9..90e2115bc5f60 100644 --- a/services/auth/auth.go +++ b/services/auth/auth.go @@ -19,7 +19,6 @@ import ( "code.gitea.io/gitea/modules/session" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web/middleware" - gitea_context "code.gitea.io/gitea/services/context" user_service "code.gitea.io/gitea/services/user" ) @@ -162,9 +161,4 @@ func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore } middleware.SetLocaleCookie(resp, user.Language, 0) - - // force to generate a new CSRF token - if ctx := gitea_context.GetWebContext(req.Context()); ctx != nil { - ctx.Csrf.PrepareForSessionUser(ctx) - } } diff --git a/services/context/api.go b/services/context/api.go index d698b9116375b..591efadf3751f 100644 --- a/services/context/api.go +++ b/services/context/api.go @@ -227,7 +227,7 @@ func APIContexter() func(http.Handler) http.Handler { ctx.SetContextValue(apiContextKey, ctx) - // If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid. + // FIXME: GLOBAL-PARSE-FORM: see more details in another FIXME comment if ctx.Req.Method == http.MethodPost && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") { if !ctx.ParseMultipartForm() { return diff --git a/services/context/context.go b/services/context/context.go index 26b5bd3775b7a..2d42c4a5fd93e 100644 --- a/services/context/context.go +++ b/services/context/context.go @@ -6,14 +6,12 @@ package context import ( "context" - "encoding/hex" "fmt" "html/template" "io" "net/http" "net/url" "strings" - "time" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" @@ -48,7 +46,6 @@ type Context struct { PageData map[string]any // data used by JavaScript modules in one page, it's `window.config.pageData` Cache cache.StringCache - Csrf CSRFProtector Flash *middleware.Flash Session session.Store @@ -143,18 +140,6 @@ func NewWebContext(base *Base, render Render, session session.Store) *Context { // Contexter initializes a classic context for a request. func Contexter() func(next http.Handler) http.Handler { rnd := templates.HTMLRenderer() - csrfOpts := CsrfOptions{ - Secret: hex.EncodeToString(setting.GetGeneralTokenSigningSecret()), - Cookie: setting.CSRFCookieName, - Secure: setting.SessionConfig.Secure, - CookieHTTPOnly: setting.CSRFCookieHTTPOnly, - CookieDomain: setting.SessionConfig.Domain, - CookiePath: setting.SessionConfig.CookiePath, - SameSite: setting.SessionConfig.SameSite, - } - 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) { base := NewBaseContext(resp, req) @@ -167,8 +152,6 @@ func Contexter() func(next http.Handler) http.Handler { ctx.PageData = map[string]any{} ctx.Data["PageData"] = ctx.PageData - ctx.Csrf = NewCSRFProtector(csrfOpts) - // get the last flash message from cookie lastFlashCookie, lastFlashMsg := middleware.GetSiteCookieFlashMessage(ctx, ctx.Req, CookieNameFlash) if vals, _ := url.ParseQuery(lastFlashCookie); len(vals) > 0 { @@ -184,7 +167,10 @@ func Contexter() func(next http.Handler) http.Handler { } }) - // If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid. + // FIXME: GLOBAL-PARSE-FORM: this ParseMultipartForm was used for parsing the csrf token from multipart/form-data + // We have dropped the csrf token, so ideally this global ParseMultipartForm should be removed. + // When removing this, we need to avoid regressions in the handler functions because Golang's http framework is quite fragile + // and developers sometimes need to manually prase the form before accessing some values. if ctx.Req.Method == http.MethodPost && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") { if !ctx.ParseMultipartForm() { return diff --git a/services/context/context_cookie.go b/services/context/context_cookie.go index b6f8dadb5665a..a28ae3b33dd43 100644 --- a/services/context/context_cookie.go +++ b/services/context/context_cookie.go @@ -25,13 +25,11 @@ func removeSessionCookieHeader(w http.ResponseWriter) { } // SetSiteCookie convenience function to set most cookies consistently -// CSRF and a few others are the exception here func (ctx *Context) SetSiteCookie(name, value string, maxAge int) { middleware.SetSiteCookie(ctx.Resp, name, value, maxAge) } // DeleteSiteCookie convenience function to delete most cookies consistently -// CSRF and a few others are the exception here func (ctx *Context) DeleteSiteCookie(name string) { middleware.SetSiteCookie(ctx.Resp, name, "", -1) } diff --git a/services/context/csrf.go b/services/context/csrf.go deleted file mode 100644 index aa99f34b0301a..0000000000000 --- a/services/context/csrf.go +++ /dev/null @@ -1,168 +0,0 @@ -// Copyright 2013 Martini Authors -// Copyright 2014 The Macaron Authors -// Copyright 2021 The Gitea Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"): you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. -// SPDX-License-Identifier: Apache-2.0 - -// a middleware that generates and validates CSRF tokens. - -package context - -import ( - "html/template" - "net/http" - "strconv" - "time" - - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/util" -) - -const ( - CsrfHeaderName = "X-Csrf-Token" - CsrfFormName = "_csrf" -) - -// CSRFProtector represents a CSRF protector and is used to get the current token and validate the token. -type CSRFProtector interface { - // PrepareForSessionUser prepares the csrf protector for the current session user. - PrepareForSessionUser(ctx *Context) - // Validate validates the csrf token in http context. - Validate(ctx *Context) - // DeleteCookie deletes the csrf cookie - DeleteCookie(ctx *Context) -} - -type csrfProtector struct { - opt CsrfOptions - // id must be unique per user. - id string - // token is the valid one which will be used by end user and passed via header, cookie, or hidden form value. - token string -} - -// CsrfOptions maintains options to manage behavior of Generate. -type CsrfOptions struct { - // The global secret value used to generate Tokens. - Secret string - // Cookie value used to set and get token. - Cookie string - // Cookie domain. - CookieDomain string - // Cookie path. - CookiePath string - CookieHTTPOnly bool - // SameSite set the cookie SameSite type - SameSite http.SameSite - // Set the Secure flag to true on the cookie. - Secure bool - // sessionKey is the key used for getting the unique ID per user. - sessionKey string - // oldSessionKey saves old value corresponding to sessionKey. - oldSessionKey string -} - -func newCsrfCookie(opt *CsrfOptions, value string) *http.Cookie { - return &http.Cookie{ - Name: opt.Cookie, - Value: value, - Path: opt.CookiePath, - Domain: opt.CookieDomain, - MaxAge: int(CsrfTokenTimeout.Seconds()), - Secure: opt.Secure, - HttpOnly: opt.CookieHTTPOnly, - SameSite: opt.SameSite, - } -} - -func NewCSRFProtector(opt CsrfOptions) CSRFProtector { - if opt.Secret == "" { - panic("CSRF secret is empty but it must be set") // it shouldn't happen because it is always set in code - } - opt.Cookie = util.IfZero(opt.Cookie, "_csrf") - opt.CookiePath = util.IfZero(opt.CookiePath, "/") - opt.sessionKey = "uid" - opt.oldSessionKey = "_old_" + opt.sessionKey - return &csrfProtector{opt: opt} -} - -func (c *csrfProtector) PrepareForSessionUser(ctx *Context) { - c.id = "0" - if uidAny := ctx.Session.Get(c.opt.sessionKey); uidAny != nil { - switch uidVal := uidAny.(type) { - case string: - c.id = uidVal - case int64: - c.id = strconv.FormatInt(uidVal, 10) - default: - log.Error("invalid uid type in session: %T", uidAny) - } - } - - oldUID := ctx.Session.Get(c.opt.oldSessionKey) - uidChanged := oldUID == nil || oldUID.(string) != c.id - cookieToken := ctx.GetSiteCookie(c.opt.Cookie) - - needsNew := true - if uidChanged { - _ = ctx.Session.Set(c.opt.oldSessionKey, c.id) - } else if cookieToken != "" { - // If cookie token present, 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 { - c.token = cookieToken - needsNew = false - } - } - } - - if needsNew { - c.token = GenerateCsrfToken(c.opt.Secret, c.id, "POST", time.Now()) - ctx.Resp.Header().Add("Set-Cookie", newCsrfCookie(&c.opt, c.token).String()) - } - - ctx.Data["CsrfToken"] = c.token - ctx.Data["CsrfTokenHtml"] = template.HTML(``) -} - -func (c *csrfProtector) validateToken(ctx *Context, token string) { - if !ValidCsrfToken(token, c.opt.Secret, c.id, "POST", time.Now()) { - c.DeleteCookie(ctx) - // currently, there should be no access to the APIPath with CSRF token. because templates shouldn't use the `/api/` endpoints. - // FIXME: distinguish what the response is for: HTML (web page) or JSON (fetch) - http.Error(ctx.Resp, "Invalid CSRF token.", http.StatusBadRequest) - } -} - -// 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, http.StatusBadRequest is sent. -func (c *csrfProtector) Validate(ctx *Context) { - if token := ctx.Req.Header.Get(CsrfHeaderName); token != "" { - c.validateToken(ctx, token) - return - } - if token := ctx.Req.FormValue(CsrfFormName); token != "" { - c.validateToken(ctx, token) - return - } - c.validateToken(ctx, "") // no csrf token, use an empty token to respond error -} - -func (c *csrfProtector) DeleteCookie(ctx *Context) { - cookie := newCsrfCookie(&c.opt, "") - cookie.MaxAge = -1 - ctx.Resp.Header().Add("Set-Cookie", cookie.String()) -} diff --git a/services/context/xsrf.go b/services/context/xsrf.go deleted file mode 100644 index 15e36d1859859..0000000000000 --- a/services/context/xsrf.go +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright 2012 Google Inc. All Rights Reserved. -// Copyright 2014 The Macaron Authors -// Copyright 2020 The Gitea Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// SPDX-License-Identifier: Apache-2.0 - -package context - -import ( - "bytes" - "crypto/hmac" - "crypto/sha1" - "crypto/subtle" - "encoding/base64" - "fmt" - "strconv" - "strings" - "time" -) - -// CsrfTokenTimeout represents the duration that XSRF tokens are valid. -// It is exported so clients may set cookie timeouts that match generated tokens. -const CsrfTokenTimeout = 24 * time.Hour - -// CsrfTokenRegenerationInterval is the interval between token generations, old tokens are still valid before CsrfTokenTimeout -var CsrfTokenRegenerationInterval = 10 * time.Minute - -var csrfTokenSep = []byte(":") - -// GenerateCsrfToken returns a URL-safe secure XSRF token that expires in CsrfTokenTimeout hours. -// key is a secret key for your application. -// userID is a unique identifier for the user. -// actionID is the action the user is taking (e.g. POSTing to a particular path). -func GenerateCsrfToken(key, userID, actionID string, now time.Time) string { - nowUnixNano := now.UnixNano() - nowUnixNanoStr := strconv.FormatInt(nowUnixNano, 10) - h := hmac.New(sha1.New, []byte(key)) - h.Write([]byte(strings.ReplaceAll(userID, ":", "_"))) - h.Write(csrfTokenSep) - h.Write([]byte(strings.ReplaceAll(actionID, ":", "_"))) - h.Write(csrfTokenSep) - h.Write([]byte(nowUnixNanoStr)) - tok := fmt.Sprintf("%s:%s", h.Sum(nil), nowUnixNanoStr) - return base64.RawURLEncoding.EncodeToString([]byte(tok)) -} - -func ParseCsrfToken(token string) (issueTime time.Time, ok bool) { - data, err := base64.RawURLEncoding.DecodeString(token) - if err != nil { - return time.Time{}, false - } - - pos := bytes.LastIndex(data, csrfTokenSep) - if pos == -1 { - return time.Time{}, false - } - nanos, err := strconv.ParseInt(string(data[pos+1:]), 10, 64) - if err != nil { - return time.Time{}, false - } - return time.Unix(0, nanos), true -} - -// ValidCsrfToken returns true if token is a valid and unexpired token returned by Generate. -func ValidCsrfToken(token, key, userID, actionID string, now time.Time) bool { - issueTime, ok := ParseCsrfToken(token) - if !ok { - return false - } - - // Check that the token is not expired. - if now.Sub(issueTime) >= CsrfTokenTimeout { - return false - } - - // Check that the token is not from the future. - // Allow 1-minute grace period in case the token is being verified on a - // machine whose clock is behind the machine that issued the token. - if issueTime.After(now.Add(1 * time.Minute)) { - return false - } - - expected := GenerateCsrfToken(key, userID, actionID, issueTime) - - // Check that the token matches the expected value. - // Use constant time comparison to avoid timing attacks. - return subtle.ConstantTimeCompare([]byte(token), []byte(expected)) == 1 -} diff --git a/services/context/xsrf_test.go b/services/context/xsrf_test.go deleted file mode 100644 index 21cda5d5d46e4..0000000000000 --- a/services/context/xsrf_test.go +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright 2012 Google Inc. All Rights Reserved. -// Copyright 2014 The Macaron Authors -// Copyright 2020 The Gitea Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// SPDX-License-Identifier: Apache-2.0 - -package context - -import ( - "encoding/base64" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -const ( - key = "quay" - userID = "12345678" - actionID = "POST /form" -) - -var ( - now = time.Now() - oneMinuteFromNow = now.Add(1 * time.Minute) -) - -func Test_ValidToken(t *testing.T) { - t.Run("Validate token", func(t *testing.T) { - tok := GenerateCsrfToken(key, userID, actionID, now) - assert.True(t, ValidCsrfToken(tok, key, userID, actionID, oneMinuteFromNow)) - assert.True(t, ValidCsrfToken(tok, key, userID, actionID, now.Add(CsrfTokenTimeout-1*time.Nanosecond))) - assert.True(t, ValidCsrfToken(tok, key, userID, actionID, now.Add(-1*time.Minute))) - }) -} - -// Test_SeparatorReplacement tests that separators are being correctly substituted -func Test_SeparatorReplacement(t *testing.T) { - t.Run("Test two separator replacements", func(t *testing.T) { - assert.NotEqual(t, GenerateCsrfToken("foo:bar", "baz", "wah", now), - GenerateCsrfToken("foo", "bar:baz", "wah", now)) - }) -} - -func Test_InvalidToken(t *testing.T) { - t.Run("Test invalid tokens", func(t *testing.T) { - invalidTokenTests := []struct { - name, key, userID, actionID string - t time.Time - }{ - {"Bad key", "foobar", userID, actionID, oneMinuteFromNow}, - {"Bad userID", key, "foobar", actionID, oneMinuteFromNow}, - {"Bad actionID", key, userID, "foobar", oneMinuteFromNow}, - {"Expired", key, userID, actionID, now.Add(CsrfTokenTimeout)}, - {"More than 1 minute from the future", key, userID, actionID, now.Add(-1*time.Nanosecond - 1*time.Minute)}, - } - - tok := GenerateCsrfToken(key, userID, actionID, now) - for _, itt := range invalidTokenTests { - assert.False(t, ValidCsrfToken(tok, itt.key, itt.userID, itt.actionID, itt.t)) - } - }) -} - -// Test_ValidateBadData primarily tests that no unexpected panics are triggered during parsing -func Test_ValidateBadData(t *testing.T) { - t.Run("Validate bad data", func(t *testing.T) { - badDataTests := []struct { - name, tok string - }{ - {"Invalid Base64", "ASDab24(@)$*=="}, - {"No delimiter", base64.URLEncoding.EncodeToString([]byte("foobar12345678"))}, - {"Invalid time", base64.URLEncoding.EncodeToString([]byte("foobar:foobar"))}, - } - - for _, bdt := range badDataTests { - assert.False(t, ValidCsrfToken(bdt.tok, key, userID, actionID, oneMinuteFromNow)) - } - }) -} diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl index 7b96b4e94fd2b..d29a52b76bde8 100644 --- a/templates/admin/auth/edit.tmpl +++ b/templates/admin/auth/edit.tmpl @@ -6,7 +6,6 @@