diff --git a/go.mod b/go.mod index de8629f1322a..95208a7df3d3 100644 --- a/go.mod +++ b/go.mod @@ -245,6 +245,7 @@ require ( github.com/prometheus/procfs v0.8.0 // indirect github.com/rivo/uniseg v0.4.2 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect + github.com/rs/cors v1.8.2 // indirect github.com/rs/xid v1.4.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect diff --git a/go.sum b/go.sum index 2cacaaa0554b..dffec4fe2b6e 100644 --- a/go.sum +++ b/go.sum @@ -1297,6 +1297,8 @@ github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4 github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= +github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= diff --git a/modules/setting/cors.go b/modules/setting/cors.go index ae0736e8307c..e2231a13a042 100644 --- a/modules/setting/cors.go +++ b/modules/setting/cors.go @@ -7,8 +7,13 @@ import ( "time" "code.gitea.io/gitea/modules/log" + "github.com/rs/cors" ) +// Cors handles CORS requests and allows other middlewares +// to check whetcher request marches CORS allowed origins. +var Cors *cors.Cors + // CORSConfig defines CORS settings var CORSConfig = struct { Enabled bool @@ -34,6 +39,13 @@ func newCORSService() { } if CORSConfig.Enabled { + Cors = cors.New(cors.Options{ + AllowedOrigins: CORSConfig.AllowDomain, + AllowedMethods: CORSConfig.Methods, + AllowCredentials: CORSConfig.AllowCredentials, + MaxAge: int(CORSConfig.MaxAge.Seconds()), + }) + log.Info("CORS Service Enabled") } } diff --git a/routers/common/middleware.go b/routers/common/middleware.go index 0c7838b0ef5b..0f22d2181c15 100644 --- a/routers/common/middleware.go +++ b/routers/common/middleware.go @@ -78,5 +78,45 @@ func Middlewares() []func(http.Handler) http.Handler { next.ServeHTTP(resp, req) }) }) + + // Add CSRF handler. + handlers = append(handlers, csrfHandler()) + return handlers } + +// csfrHandler blocks recognized CSRF attempts. +// WARNING: for this proctection to work, web browser compatible with +// Fetch Metadata Request Headers (https://w3c.github.io/webappsec-fetch-metadata) +// must be used. +func csrfHandler() func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + + // Put header names we use for CSRF recognition into Vary response header. + if setting.CORSConfig.Enabled { + resp.Header().Set("Vary", "Origin, Sec-Fetch-Site") + } else { + resp.Header().Set("Vary", "Sec-Fetch-Site") + } + + // Allow requests not recognized as CSRF. + secFetchSite := strings.ToLower(req.Header.Get("Sec-Fetch-Site")) + if req.Method == "GET" || // GET, HEAD and OPTIONS must not be used for changing state (CSRF resistant). + req.Method == "HEAD" || + req.Method == "OPTIONS" || + secFetchSite == "" || // Accept requests from clients without Fetch Metadata Request Headers support. + secFetchSite == "same-origin" || // Accept requests from own origin. + secFetchSite == "none" || // Accept requests initiated by user (i.e. using bookmark). + ((secFetchSite == "same-site" || secFetchSite == "cross-site") && // Accept cross site requests allowed by CORS. + setting.CORSConfig.Enabled && setting.Cors.OriginAllowed(req)) { + next.ServeHTTP(resp, req) + return + } + + // Forbid and log other requests as CSRF. + log.Error("CSRF rejected: METHOD=\"%s\", Origin=\"%s\", Sec-Fetch-Site=\"%s\"", req.Method, req.Header.Get("Origin"), secFetchSite) + http.Error(resp, http.StatusText(http.StatusForbidden), http.StatusForbidden) + }) + } +}