merged and backport to 8.5 (#50104)

Co-authored-by: Emil Tullstedt <emil.tullstedt@grafana.com>
This commit is contained in:
Eric Leijonmarck
2022-06-02 16:44:17 +02:00
committed by GitHub
co-authored by Emil Tullstedt
parent 1c2e240995
commit f8c6bba2ce
5 changed files with 149 additions and 58 deletions
+9 -4
View File
@@ -13,6 +13,11 @@ import (
"strings"
"sync"
"github.com/grafana/grafana/pkg/middleware/csrf"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/grafana/grafana/pkg/api/avatar"
"github.com/grafana/grafana/pkg/api/routing"
httpstatic "github.com/grafana/grafana/pkg/api/static"
@@ -70,8 +75,6 @@ import (
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util/errutil"
"github.com/grafana/grafana/pkg/web"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
type HTTPServer struct {
@@ -147,6 +150,7 @@ type HTTPServer struct {
DashboardsnapshotsService *dashboardsnapshots.Service
PluginSettings *pluginSettings.Service
AvatarCacheServer *avatar.AvatarCacheServer
Csrf csrf.Service
}
type ServerOptions struct {
@@ -178,7 +182,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
dashboardProvisioningService dashboards.DashboardProvisioningService, folderService dashboards.FolderService,
datasourcePermissionsService permissions.DatasourcePermissionsService, alertNotificationService *alerting.AlertNotificationService,
dashboardsnapshotsService *dashboardsnapshots.Service, commentsService *comments.Service, pluginSettings *pluginSettings.Service,
avatarCacheServer *avatar.AvatarCacheServer,
avatarCacheServer *avatar.AvatarCacheServer, csrfService csrf.Service,
) (*HTTPServer, error) {
web.Env = cfg.Env
m := web.New()
@@ -251,6 +255,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
PluginSettings: pluginSettings,
permissionServices: permissionsServices,
AvatarCacheServer: avatarCacheServer,
Csrf: csrfService,
}
if hs.Listener != nil {
hs.log.Debug("Using provided listener")
@@ -478,7 +483,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
}
m.Use(middleware.Recovery(hs.Cfg))
m.UseMiddleware(middleware.CSRF(hs.Cfg.LoginCookieName, hs.log))
m.UseMiddleware(hs.Csrf.Middleware(hs.log))
hs.mapStatic(m, hs.Cfg.StaticRootPath, "build", "public/build")
hs.mapStatic(m, hs.Cfg.StaticRootPath, "", "public", "/public/views/swagger.html")
-50
View File
@@ -1,50 +0,0 @@
package middleware
import (
"errors"
"net/http"
"net/url"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/util"
)
func CSRF(loginCookieName string, logger log.Logger) func(http.Handler) http.Handler {
// As per RFC 7231/4.2.2 these methods are idempotent:
// (GET is excluded because it may have side effects in some APIs)
safeMethods := []string{"HEAD", "OPTIONS", "TRACE"}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// If request has no login cookie - skip CSRF checks
if _, err := r.Cookie(loginCookieName); errors.Is(err, http.ErrNoCookie) {
next.ServeHTTP(w, r)
return
}
// Skip CSRF checks for "safe" methods
for _, method := range safeMethods {
if r.Method == method {
next.ServeHTTP(w, r)
return
}
}
// Otherwise - verify that Origin matches the server origin
netAddr, err := util.SplitHostPortDefault(r.Host, "", "0") // we ignore the port
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
origin, err := url.Parse(r.Header.Get("Origin"))
if err != nil {
logger.Error("error parsing Origin header", "err", err)
}
if err != nil || netAddr.Host == "" || (origin.String() != "" && origin.Hostname() != netAddr.Host) {
http.Error(w, "origin not allowed", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}
+130
View File
@@ -0,0 +1,130 @@
package csrf
import (
"errors"
"net/http"
"net/url"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
type Service interface {
Middleware(logger log.Logger) func(http.Handler) http.Handler
TrustOrigin(origin string)
AddOriginHeader(headerName string)
AddSafeEndpoint(endpoint string)
}
type Implementation struct {
cfg *setting.Cfg
trustedOrigins map[string]struct{}
originHeaders map[string]struct{}
safeEndpoints map[string]struct{}
}
func ProvideCSRFFilter(cfg *setting.Cfg) Service {
i := &Implementation{
cfg: cfg,
trustedOrigins: map[string]struct{}{},
originHeaders: map[string]struct{}{
"Origin": {},
},
safeEndpoints: map[string]struct{}{},
}
additionalHeaders := cfg.SectionWithEnvOverrides("security").Key("csrf_additional_headers").Strings(" ")
trustedOrigins := cfg.SectionWithEnvOverrides("security").Key("csrf_trusted_origins").Strings(" ")
for _, header := range additionalHeaders {
i.originHeaders[header] = struct{}{}
}
for _, origin := range trustedOrigins {
i.trustedOrigins[origin] = struct{}{}
}
return i
}
func (i *Implementation) Middleware(logger log.Logger) func(http.Handler) http.Handler {
// As per RFC 7231/4.2.2 these methods are idempotent:
// (GET is excluded because it may have side effects in some APIs)
safeMethods := []string{"HEAD", "OPTIONS", "TRACE"}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// If request has no login cookie - skip CSRF checks
if _, err := r.Cookie(i.cfg.LoginCookieName); errors.Is(err, http.ErrNoCookie) {
next.ServeHTTP(w, r)
return
}
// Skip CSRF checks for "safe" methods
for _, method := range safeMethods {
if r.Method == method {
next.ServeHTTP(w, r)
return
}
}
// Skip CSRF checks for "safe" endpoints
for safeEndpoint := range i.safeEndpoints {
if r.URL.Path == safeEndpoint {
next.ServeHTTP(w, r)
return
}
}
// Otherwise - verify that Origin matches the server origin
netAddr, err := util.SplitHostPortDefault(r.Host, "", "0") // we ignore the port
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
origins := map[string]struct{}{}
for header := range i.originHeaders {
origin, err := url.Parse(r.Header.Get(header))
if err != nil {
logger.Error("error parsing Origin header", "header", header, "err", err)
}
if origin.String() != "" {
origins[origin.Hostname()] = struct{}{}
}
}
// No Origin header sent, skip CSRF check.
if len(origins) == 0 {
next.ServeHTTP(w, r)
return
}
trustedOrigin := false
for o := range i.trustedOrigins {
if _, ok := origins[o]; ok {
trustedOrigin = true
break
}
}
_, hostnameMatches := origins[netAddr.Host]
if netAddr.Host == "" || !trustedOrigin && !hostnameMatches {
http.Error(w, "origin not allowed", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}
func (i *Implementation) TrustOrigin(origin string) {
i.trustedOrigins[origin] = struct{}{}
}
func (i *Implementation) AddOriginHeader(headerName string) {
i.originHeaders[headerName] = struct{}{}
}
// AddSafeEndpoint is used for endpoints requests to skip CSRF check
func (i *Implementation) AddSafeEndpoint(endpoint string) {
i.safeEndpoints[endpoint] = struct{}{}
}
@@ -1,12 +1,14 @@
package middleware
package csrf
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/setting"
)
func TestMiddlewareCSRF(t *testing.T) {
@@ -118,7 +120,10 @@ func csrfScenario(t *testing.T, cookieName, method, origin, host string) *httpte
})
rr := httptest.NewRecorder()
handler := CSRF(cookieName, log.New())(testHandler)
cfg := setting.NewCfg()
cfg.LoginCookieName = cookieName
service := ProvideCSRFFilter(cfg)
handler := service.Middleware(log.New())(testHandler)
handler.ServeHTTP(rr, req)
return rr
}
+2 -1
View File
@@ -5,7 +5,6 @@ package server
import (
"github.com/google/wire"
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/api"
"github.com/grafana/grafana/pkg/api/avatar"
@@ -25,6 +24,7 @@ import (
"github.com/grafana/grafana/pkg/infra/usagestats/statscollector"
loginpkg "github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/middleware/csrf"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
@@ -238,6 +238,7 @@ var wireBasicSet = wire.NewSet(
avatar.ProvideAvatarCacheServer,
authproxy.ProvideAuthProxy,
statscollector.ProvideService,
csrf.ProvideCSRFFilter,
)
var wireSet = wire.NewSet(