Replace CSRF cookie with CrossOriginProtection (#36183)

Removes the CSRF cookie in favor of
[`CrossOriginProtection`](https://pkg.go.dev/net/http#CrossOriginProtection)
which relies purely on HTTP headers.

Fixes: https://github.com/go-gitea/gitea/issues/11188
Fixes: https://github.com/go-gitea/gitea/issues/30333
Helps: https://github.com/go-gitea/gitea/issues/35107

TODOs:

- [x] Fix tests
- [ ] Ideally add tests to validates the protection

---------

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
silverwind
2025-12-25 11:33:34 +01:00
committed by GitHub
parent eddf875992
commit 42d294941c
207 changed files with 178 additions and 1196 deletions
+4 -4
View File
@@ -38,8 +38,8 @@ func AuthShared(ctx *context.Base, sessionStore auth_service.SessionStore, authM
// VerifyOptions contains required or check options
type VerifyOptions struct {
SignInRequired bool
SignOutRequired bool
AdminRequired bool
DisableCSRF bool
SignInRequired bool
SignOutRequired bool
AdminRequired bool
DisableCrossOriginProtection bool
}
-7
View File
@@ -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
-3
View File
@@ -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
+1 -1
View File
@@ -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())
}, repo.HTTPGitEnabledHandler, repo.CorsHandler(), optSignInFromAnyOrigin, context.UserAssignmentWeb())
}
+1 -1
View File
@@ -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{
+20 -13
View File
@@ -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) {
crossOriginProtection := 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.DisableCrossOriginProtection {
if err := crossOriginProtection.Check(ctx.Req); err != nil {
http.Error(ctx.Resp, err.Error(), http.StatusForbidden)
return
}
}
@@ -292,7 +292,12 @@ func Routes() *web.Router {
return routes
}
var optSignInIgnoreCsrf = verifyAuthWithOptions(&common.VerifyOptions{DisableCSRF: true})
// optSignInFromAnyOrigin means that the user can (optionally) be signed in from any origin (no cross-origin protection)
// - With CORS middleware: CORS middleware does the preflight request handling, the requests has Sec-Fetch-Site header.
// The CORS mechanism already protects cross-origin requests, and the CrossOriginProtection has no "allowed origin" list, so disable CrossOriginProtection.
// - For non-browser client requests: git clone via http, no Sec-Fetch-Site header.
// Such requests are not cross-origin requests, so disable CrossOriginProtection.
var optSignInFromAnyOrigin = verifyAuthWithOptions(&common.VerifyOptions{DisableCrossOriginProtection: true})
// registerWebRoutes register routes
func registerWebRoutes(m *web.Router) {
@@ -489,7 +494,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", optSignIn, misc.WebThemeApply)
m.Group("/explore", func() {
m.Get("", func(ctx *context.Context) {
@@ -565,12 +570,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)
}, optionsCorsHandler(), optSignInFromAnyOrigin)
}, oauth2Enabled)
m.Group("/user/settings", func() {
@@ -1653,7 +1660,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, lfsServerEnabled, repo.CorsHandler(), optSignInFromAnyOrigin) // "/{username}/{reponame}/{lfs-paths}": git-lfs support, see also addOwnerRepoGitHTTPRouters
addOwnerRepoGitHTTPRouters(m) // "/{username}/{reponame}/{git-paths}": git http support