From 98aa40cd034bf50c6cc4bb814ce754162d0d9ad5 Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 17 Dec 2025 23:32:12 +0100 Subject: [PATCH 01/21] Replace csrf cookie with Go 1.25 CrossOriginProtection --- modules/setting/markup.go | 2 +- modules/setting/oauth2.go | 2 +- modules/setting/security.go | 3 - routers/web/auth/auth.go | 7 - routers/web/auth/oauth.go | 3 - routers/web/user/setting/keys.go | 2 +- routers/web/web.go | 22 +-- services/auth/auth.go | 6 - services/context/api.go | 2 +- services/context/context.go | 19 +- services/context/context_cookie.go | 2 - services/context/csrf.go | 168 ------------------ services/context/xsrf.go | 99 ----------- services/context/xsrf_test.go | 91 ---------- templates/admin/auth/edit.tmpl | 1 - templates/admin/auth/new.tmpl | 1 - templates/admin/config.tmpl | 2 - templates/admin/cron.tmpl | 1 - templates/admin/dashboard.tmpl | 1 - templates/admin/emails/list.tmpl | 1 - templates/admin/notice.tmpl | 1 - templates/admin/packages/list.tmpl | 2 - templates/admin/queue_manage.tmpl | 2 - templates/admin/repo/list.tmpl | 1 - templates/admin/repo/unadopted.tmpl | 2 - templates/admin/user/edit.tmpl | 3 - templates/admin/user/new.tmpl | 1 - templates/base/head.tmpl | 2 +- templates/base/head_navbar.tmpl | 2 - templates/base/head_script.tmpl | 1 - templates/org/create.tmpl | 1 - templates/org/settings/options.tmpl | 2 - .../org/settings/options_dangerzone.tmpl | 3 - templates/org/team/invite.tmpl | 1 - templates/org/team/members.tmpl | 2 - templates/org/team/new.tmpl | 1 - templates/org/team/repositories.tmpl | 2 - templates/org/team/sidebar.tmpl | 1 - templates/org/team/teams.tmpl | 1 - templates/package/settings.tmpl | 2 - templates/package/shared/cargo.tmpl | 2 - .../package/shared/cleanup_rules/edit.tmpl | 1 - templates/projects/new.tmpl | 1 - templates/repo/actions/workflow_dispatch.tmpl | 1 - templates/repo/branch/list.tmpl | 2 - templates/repo/commit_page.tmpl | 2 - templates/repo/create.tmpl | 1 - templates/repo/diff/comment_form.tmpl | 1 - templates/repo/diff/new_review.tmpl | 1 - templates/repo/editor/cherry_pick.tmpl | 1 - templates/repo/editor/delete.tmpl | 1 - templates/repo/editor/edit.tmpl | 1 - templates/repo/editor/fork.tmpl | 1 - templates/repo/editor/patch.tmpl | 1 - templates/repo/editor/upload.tmpl | 1 - templates/repo/header.tmpl | 2 - .../repo/issue/labels/label_edit_modal.tmpl | 1 - .../issue/labels/label_load_template.tmpl | 1 - templates/repo/issue/milestone_new.tmpl | 1 - templates/repo/issue/new_form.tmpl | 1 - templates/repo/issue/sidebar/due_date.tmpl | 1 - .../issue/sidebar/issue_dependencies.tmpl | 2 - .../repo/issue/sidebar/issue_management.tmpl | 3 - .../repo/issue/sidebar/reviewer_list.tmpl | 1 - .../issue/sidebar/stopwatch_timetracker.tmpl | 2 - templates/repo/issue/view_content.tmpl | 1 - .../issue/view_content/pull_merge_box.tmpl | 1 - .../view_content/reference_issue_dialog.tmpl | 1 - .../view_content/update_branch_by_merge.tmpl | 1 - templates/repo/migrate/codebase.tmpl | 1 - templates/repo/migrate/codecommit.tmpl | 1 - templates/repo/migrate/git.tmpl | 1 - templates/repo/migrate/gitbucket.tmpl | 1 - templates/repo/migrate/gitea.tmpl | 1 - templates/repo/migrate/github.tmpl | 1 - templates/repo/migrate/gitlab.tmpl | 1 - templates/repo/migrate/gogs.tmpl | 1 - templates/repo/migrate/migrating.tmpl | 2 - templates/repo/migrate/onedev.tmpl | 1 - templates/repo/pulls/fork.tmpl | 1 - templates/repo/release/new.tmpl | 1 - templates/repo/settings/actions_general.tmpl | 2 - templates/repo/settings/branches.tmpl | 1 - templates/repo/settings/collaboration.tmpl | 2 - templates/repo/settings/deploy_keys.tmpl | 1 - templates/repo/settings/githook_edit.tmpl | 1 - templates/repo/settings/lfs.tmpl | 1 - templates/repo/settings/lfs_locks.tmpl | 2 - templates/repo/settings/lfs_pointers.tmpl | 1 - templates/repo/settings/options.tmpl | 19 -- templates/repo/settings/protected_branch.tmpl | 1 - templates/repo/settings/public_access.tmpl | 1 - .../repo/settings/push_mirror_sync_modal.tmpl | 1 - templates/repo/settings/tags.tmpl | 2 - templates/repo/settings/webhook/dingtalk.tmpl | 1 - templates/repo/settings/webhook/discord.tmpl | 1 - templates/repo/settings/webhook/feishu.tmpl | 1 - templates/repo/settings/webhook/gitea.tmpl | 1 - templates/repo/settings/webhook/gogs.tmpl | 1 - templates/repo/settings/webhook/history.tmpl | 1 - templates/repo/settings/webhook/matrix.tmpl | 1 - templates/repo/settings/webhook/msteams.tmpl | 1 - .../repo/settings/webhook/packagist.tmpl | 1 - templates/repo/settings/webhook/slack.tmpl | 1 - templates/repo/settings/webhook/telegram.tmpl | 1 - .../repo/settings/webhook/wechatwork.tmpl | 1 - templates/repo/wiki/new.tmpl | 1 - templates/shared/actions/runner_edit.tmpl | 1 - templates/shared/secrets/add_list.tmpl | 1 - templates/shared/user/block_user_dialog.tmpl | 1 - templates/shared/user/blocked_users.tmpl | 3 - templates/shared/variables/variable_list.tmpl | 1 - templates/user/auth/activate.tmpl | 1 - templates/user/auth/change_passwd_inner.tmpl | 1 - templates/user/auth/forgot_passwd.tmpl | 1 - templates/user/auth/grant.tmpl | 1 - templates/user/auth/reset_passwd.tmpl | 1 - templates/user/auth/signin_inner.tmpl | 1 - templates/user/auth/signin_openid.tmpl | 1 - templates/user/auth/signup_inner.tmpl | 1 - .../user/auth/signup_openid_connect.tmpl | 1 - .../user/auth/signup_openid_register.tmpl | 1 - templates/user/auth/twofa.tmpl | 1 - templates/user/auth/twofa_scratch.tmpl | 1 - .../user/notification/notification_div.tmpl | 2 - templates/user/settings/account.tmpl | 5 - templates/user/settings/appearance.tmpl | 3 - templates/user/settings/applications.tmpl | 1 - .../applications_oauth2_edit_form.tmpl | 3 - .../settings/applications_oauth2_list.tmpl | 1 - templates/user/settings/keys_gpg.tmpl | 2 - templates/user/settings/keys_principal.tmpl | 1 - templates/user/settings/keys_ssh.tmpl | 2 - templates/user/settings/notifications.tmpl | 2 - templates/user/settings/organization.tmpl | 1 - templates/user/settings/packages.tmpl | 1 - templates/user/settings/profile.tmpl | 2 - templates/user/settings/repos.tmpl | 2 - templates/user/settings/security/openid.tmpl | 2 - templates/user/settings/security/twofa.tmpl | 2 - .../user/settings/security/twofa_enroll.tmpl | 1 - tests/integration/actions_approve_test.go | 4 +- tests/integration/actions_concurrency_test.go | 60 ++----- tests/integration/actions_delete_run_test.go | 24 +-- tests/integration/actions_inputs_test.go | 5 +- tests/integration/actions_rerun_test.go | 16 +- .../integration/actions_runner_modify_test.go | 5 +- tests/integration/actions_settings_test.go | 5 +- tests/integration/actions_trigger_test.go | 4 +- tests/integration/actions_variables_test.go | 9 +- tests/integration/admin_user_test.go | 7 +- tests/integration/api_httpsig_test.go | 2 - .../api_packages_container_test.go | 2 - tests/integration/api_repo_languages_test.go | 1 - tests/integration/api_repo_license_test.go | 1 - tests/integration/attachment_test.go | 10 +- tests/integration/auth_ldap_test.go | 16 +- tests/integration/branches_test.go | 4 +- .../integration/change_default_branch_test.go | 6 - tests/integration/create_no_session_test.go | 2 - tests/integration/csrf_test.go | 34 ---- tests/integration/delete_user_test.go | 10 +- tests/integration/editor_test.go | 9 - tests/integration/empty_repo_test.go | 4 - tests/integration/git_general_test.go | 9 +- tests/integration/html_helper.go | 5 - tests/integration/integration_test.go | 27 +-- tests/integration/issue_test.go | 47 +---- tests/integration/migrate_test.go | 1 - tests/integration/mirror_push_test.go | 3 - tests/integration/nonascii_branches_test.go | 2 - tests/integration/oauth_test.go | 1 - tests/integration/org_project_test.go | 2 - tests/integration/org_team_invite_test.go | 31 +--- tests/integration/privateactivity_test.go | 1 - tests/integration/project_test.go | 6 +- tests/integration/pull_comment_test.go | 1 - tests/integration/pull_compare_test.go | 2 - tests/integration/pull_create_test.go | 21 +-- tests/integration/pull_merge_test.go | 36 +--- tests/integration/pull_review_test.go | 28 +-- tests/integration/pull_status_test.go | 3 - tests/integration/release_test.go | 1 - tests/integration/rename_branch_test.go | 30 +--- tests/integration/repo_branch_test.go | 14 -- tests/integration/repo_fork_test.go | 1 - tests/integration/repo_generate_test.go | 1 - tests/integration/repo_merge_upstream_test.go | 8 +- tests/integration/repo_migrate_test.go | 1 - tests/integration/repo_test.go | 5 +- tests/integration/repo_webhook_test.go | 16 +- tests/integration/timetracking_test.go | 8 +- tests/integration/user_avatar_test.go | 2 - tests/integration/user_settings_test.go | 53 +----- tests/integration/user_test.go | 7 - tests/integration/xss_test.go | 1 - .../js/components/PullRequestMergeForm.vue | 4 +- .../js/components/RepoBranchTagSelector.vue | 2 - web_src/js/features/dropzone.ts | 3 +- web_src/js/features/pull-view-file.ts | 2 +- web_src/js/features/repo-settings.ts | 3 +- web_src/js/modules/fetch.ts | 6 - web_src/js/types.ts | 1 - web_src/js/vitest.setup.ts | 1 - 204 files changed, 118 insertions(+), 1136 deletions(-) delete mode 100644 services/context/csrf.go delete mode 100644 services/context/xsrf.go delete mode 100644 services/context/xsrf_test.go delete mode 100644 tests/integration/csrf_test.go diff --git a/modules/setting/markup.go b/modules/setting/markup.go index e105506fc068c..01ec444fc750d 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, and it makes the JS in iframe can access parent window's config 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/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/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..e2a43552df2db 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 && !options.DisableCSRF { + if err := crossOrginProtection.Check(ctx.Req); err != nil { + http.Error(ctx.Resp, err.Error(), http.StatusForbidden) return } } @@ -565,12 +565,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{}), optSignInIgnoreCsrf, auth.AccessTokenOAuth) + m.Methods("GET, OPTIONS", "/keys", auth.OIDCKeys) + m.Methods("POST, OPTIONS", "/introspect", web.Bind(forms.IntrospectTokenForm{}), auth.IntrospectOAuth) + }, optionsCorsHandler(), optSignInIgnoreCsrf) }, oauth2Enabled) m.Group("/user/settings", func() { 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..f2d1ff6675808 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. + // If request sends files, parse them here otherwise the Query() can't be parsed. 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..1a7311c7b4483 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,7 @@ 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. + // If request sends files, parse them here otherwise the Query() can't be parsed 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 @@
{{template "base/disable_form_autofill"}} - {{.CsrfTokenHtml}}
diff --git a/templates/admin/auth/new.tmpl b/templates/admin/auth/new.tmpl index be4995c7846ce..29eea06f55674 100644 --- a/templates/admin/auth/new.tmpl +++ b/templates/admin/auth/new.tmpl @@ -6,7 +6,6 @@
{{template "base/disable_form_autofill"}} - {{.CsrfTokenHtml}}
diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index 080b2cd3d6bab..57631fd9c68e7 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -228,7 +228,6 @@
{{ctx.Locale.Tr "admin.config.send_test_mail"}}
- {{.CsrfTokenHtml}}
@@ -260,7 +259,6 @@
{{ctx.Locale.Tr "admin.config.cache_test"}}
- {{.CsrfTokenHtml}}
diff --git a/templates/admin/cron.tmpl b/templates/admin/cron.tmpl index 309cbce814d40..4d01ce51eb6d9 100644 --- a/templates/admin/cron.tmpl +++ b/templates/admin/cron.tmpl @@ -32,7 +32,6 @@ - {{.CsrfTokenHtml}}
diff --git a/templates/admin/dashboard.tmpl b/templates/admin/dashboard.tmpl index 2426a43b154d9..834c56672f04a 100644 --- a/templates/admin/dashboard.tmpl +++ b/templates/admin/dashboard.tmpl @@ -10,7 +10,6 @@
- {{.CsrfTokenHtml}} diff --git a/templates/admin/emails/list.tmpl b/templates/admin/emails/list.tmpl index b4335aeeec9f2..9bfef64f3c58e 100644 --- a/templates/admin/emails/list.tmpl +++ b/templates/admin/emails/list.tmpl @@ -83,7 +83,6 @@

{{ctx.Locale.Tr "admin.emails.change_email_text"}}

- {{$.CsrfTokenHtml}} diff --git a/templates/admin/notice.tmpl b/templates/admin/notice.tmpl index 6231369d39f6d..0499b0adbb85f 100644 --- a/templates/admin/notice.tmpl +++ b/templates/admin/notice.tmpl @@ -34,7 +34,6 @@ diff --git a/templates/repo/settings/lfs_pointers.tmpl b/templates/repo/settings/lfs_pointers.tmpl index 4cfc0fc6730a0..2138aadc53c72 100644 --- a/templates/repo/settings/lfs_pointers.tmpl +++ b/templates/repo/settings/lfs_pointers.tmpl @@ -5,7 +5,6 @@ {{if gt .NumAssociatable 0}}
- {{.CsrfTokenHtml}} {{range .Pointers}} {{if .Associatable}} diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index b4680431b8acf..c7b3c31b0f1f5 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -6,7 +6,6 @@
{{template "base/disable_form_autofill"}} - {{.CsrfTokenHtml}}
@@ -38,7 +37,6 @@
- {{.CsrfTokenHtml}}
{{template "shared/avatar_upload_crop" dict "LabelText" (ctx.Locale.Tr "settings.choose_new_avatar")}}
@@ -119,7 +117,6 @@
- {{.CsrfTokenHtml}} {{DateUtils.TimeSince .Created}} - {{$.CsrfTokenHtml}} {{DateUtils.FullTime .PullMirror.UpdatedUnix}} - {{.CsrfTokenHtml}} @@ -129,7 +126,6 @@
{{template "base/disable_form_autofill"}} - {{.CsrfTokenHtml}}
@@ -226,13 +222,11 @@ {{svg "octicon-pencil" 14}} - {{$.CsrfTokenHtml}}
- {{$.CsrfTokenHtml}} @@ -249,7 +243,6 @@
{{template "base/disable_form_autofill"}} - {{.CsrfTokenHtml}}
@@ -299,7 +292,6 @@
- {{.CsrfTokenHtml}} {{$isCodeEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeCode}} @@ -647,7 +639,6 @@
- {{.CsrfTokenHtml}}

@@ -694,7 +685,6 @@
- {{.CsrfTokenHtml}}
@@ -710,7 +700,6 @@
- {{.CsrfTokenHtml}} {{if .IsRepoIndexerEnabled}}

{{ctx.Locale.Tr "repo.settings.admin_code_indexer"}}

@@ -815,7 +804,6 @@
{{if .RepoTransfer}} - {{.CsrfTokenHtml}} @@ -883,7 +871,6 @@ {{ctx.Locale.Tr "repo.settings.convert_notices_1"}}
- {{.CsrfTokenHtml}}
- {{.CsrfTokenHtml}}
- {{.CsrfTokenHtml}}
- {{.CsrfTokenHtml}}
- {{.CsrfTokenHtml}} {{template "base/modal_actions_confirm" .}} @@ -1045,7 +1028,6 @@ {{ctx.Locale.Tr "repo.settings.wiki_delete_notices_1" .Repository.Name}}
- {{.CsrfTokenHtml}}
- {{.CsrfTokenHtml}} {{template "base/modal_actions_confirm" .}} diff --git a/templates/repo/settings/protected_branch.tmpl b/templates/repo/settings/protected_branch.tmpl index 3c311c18c39ee..daa1d5f3f913b 100644 --- a/templates/repo/settings/protected_branch.tmpl +++ b/templates/repo/settings/protected_branch.tmpl @@ -27,7 +27,6 @@

{{ctx.Locale.Tr "repo.settings.protect_unprotected_file_patterns_desc" "https://pkg.go.dev/github.com/gobwas/glob#Compile" "github.com/gobwas/glob"}}

- {{.CsrfTokenHtml}}
{{ctx.Locale.Tr "repo.settings.event_push"}}
diff --git a/templates/repo/settings/public_access.tmpl b/templates/repo/settings/public_access.tmpl index c1c198bcce5f2..cfd9bf5cb17f0 100644 --- a/templates/repo/settings/public_access.tmpl +++ b/templates/repo/settings/public_access.tmpl @@ -12,7 +12,6 @@ {{$paEveryoneRead := "everyone-read"}} {{$paEveryoneWrite := "everyone-write"}} - {{.CsrfTokenHtml}} diff --git a/templates/repo/settings/push_mirror_sync_modal.tmpl b/templates/repo/settings/push_mirror_sync_modal.tmpl index 3bd624fab799c..0ae7393ca9003 100644 --- a/templates/repo/settings/push_mirror_sync_modal.tmpl +++ b/templates/repo/settings/push_mirror_sync_modal.tmpl @@ -3,7 +3,6 @@ {{ctx.Locale.Tr "repo.settings.mirror_settings.push_mirror.edit_sync_time"}} - {{.CsrfTokenHtml}}
diff --git a/templates/repo/settings/tags.tmpl b/templates/repo/settings/tags.tmpl index 12ec9102b75af..af03b3598f500 100644 --- a/templates/repo/settings/tags.tmpl +++ b/templates/repo/settings/tags.tmpl @@ -14,7 +14,6 @@
- {{.CsrfTokenHtml}}
{{ctx.Locale.Tr "edit"}} - {{$.CsrfTokenHtml}} diff --git a/templates/repo/settings/webhook/dingtalk.tmpl b/templates/repo/settings/webhook/dingtalk.tmpl index dd208cde1772c..ef96972a0af9f 100644 --- a/templates/repo/settings/webhook/dingtalk.tmpl +++ b/templates/repo/settings/webhook/dingtalk.tmpl @@ -1,7 +1,6 @@ {{if eq .HookType "dingtalk"}}

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://dingtalk.com" (ctx.Locale.Tr "repo.settings.web_hook_name_dingtalk")}}

- {{.CsrfTokenHtml}}
diff --git a/templates/repo/settings/webhook/discord.tmpl b/templates/repo/settings/webhook/discord.tmpl index fa66249fa522d..9dca83a4972f5 100644 --- a/templates/repo/settings/webhook/discord.tmpl +++ b/templates/repo/settings/webhook/discord.tmpl @@ -1,7 +1,6 @@ {{if eq .HookType "discord"}}

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://discord.com" (ctx.Locale.Tr "repo.settings.web_hook_name_discord")}}

- {{.CsrfTokenHtml}}
diff --git a/templates/repo/settings/webhook/feishu.tmpl b/templates/repo/settings/webhook/feishu.tmpl index 13bd0d92a18a7..84ffff364f63c 100644 --- a/templates/repo/settings/webhook/feishu.tmpl +++ b/templates/repo/settings/webhook/feishu.tmpl @@ -4,7 +4,6 @@ {{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://larksuite.com" (ctx.Locale.Tr "repo.settings.web_hook_name_larksuite")}}

- {{.CsrfTokenHtml}}
diff --git a/templates/repo/settings/webhook/gitea.tmpl b/templates/repo/settings/webhook/gitea.tmpl index 30f14d609bad3..9f5f86d9eed94 100644 --- a/templates/repo/settings/webhook/gitea.tmpl +++ b/templates/repo/settings/webhook/gitea.tmpl @@ -2,7 +2,6 @@

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://docs.gitea.com/usage/webhooks" (ctx.Locale.Tr "repo.settings.web_hook_name_gitea")}}

{{template "base/disable_form_autofill"}} - {{.CsrfTokenHtml}}
diff --git a/templates/repo/settings/webhook/gogs.tmpl b/templates/repo/settings/webhook/gogs.tmpl index c0e054602ab08..f08114f6541e4 100644 --- a/templates/repo/settings/webhook/gogs.tmpl +++ b/templates/repo/settings/webhook/gogs.tmpl @@ -2,7 +2,6 @@

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://docs.gitea.com/usage/webhooks" (ctx.Locale.Tr "repo.settings.web_hook_name_gogs")}}

{{template "base/disable_form_autofill"}} - {{.CsrfTokenHtml}}
diff --git a/templates/repo/settings/webhook/history.tmpl b/templates/repo/settings/webhook/history.tmpl index d27c1fb8b12cd..fb23c7634ff0d 100644 --- a/templates/repo/settings/webhook/history.tmpl +++ b/templates/repo/settings/webhook/history.tmpl @@ -52,7 +52,6 @@ {{if or $.Permission.IsAdmin $.IsOrganizationOwner $.PageIsAdmin $.PageIsUserSettings}}