grafana/pkg/middleware/csrf/csrf_test.go
Bruno d4715a6f04
CSRF middleware: Add flag to skip login cookie check (#66806)
* CSRF middleware: add flag to skip login cookie check

* Update docs/sources/setup-grafana/configure-grafana/_index.md

Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com>

---------

Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com>
2023-04-24 10:11:08 -03:00

332 lines
9.0 KiB
Go

package csrf
import (
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/setting"
)
func TestMiddlewareCSRF(t *testing.T) {
tests := []struct {
name string
cookieName string
method string
origin string
host string
code int
}{
{
name: "mismatched origin and host is forbidden",
cookieName: "foo",
method: "GET",
origin: "http://notLocalhost",
host: "localhost",
code: http.StatusForbidden,
},
{
name: "mismatched origin and host is NOT forbidden with a 'Safe Method'",
cookieName: "foo",
method: "TRACE",
origin: "http://notLocalhost",
host: "localhost",
code: http.StatusOK,
},
{
name: "mismatched origin and host is NOT forbidden without a cookie",
cookieName: "",
method: "GET",
origin: "http://notLocalhost",
host: "localhost",
code: http.StatusOK,
},
{
name: "malformed host is a bad request",
cookieName: "foo",
method: "GET",
host: "localhost:80:80",
code: http.StatusBadRequest,
},
{
name: "host works without port",
cookieName: "foo",
method: "GET",
host: "localhost",
origin: "http://localhost",
code: http.StatusOK,
},
{
name: "port does not have to match",
cookieName: "foo",
method: "GET",
host: "localhost:80",
origin: "http://localhost:3000",
code: http.StatusOK,
},
{
name: "IPv6 host works with port",
cookieName: "foo",
method: "GET",
host: "[::1]:3000",
origin: "http://[::1]:3000",
code: http.StatusOK,
},
{
name: "IPv6 host (with longer address) works with port",
cookieName: "foo",
method: "GET",
host: "[2001:db8::1]:3000",
origin: "http://[2001:db8::1]:3000",
code: http.StatusOK,
},
{
name: "IPv6 host (with longer address) works without port",
cookieName: "foo",
method: "GET",
host: "[2001:db8::1]",
origin: "http://[2001:db8::1]",
code: http.StatusOK,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rr := csrfScenario(t, tt.cookieName, tt.method, tt.origin, tt.host)
require.Equal(t, tt.code, rr.Code)
})
}
}
func TestCSRF_Check(t *testing.T) {
tests := []struct {
name string
request *http.Request
getCfg func() *setting.Cfg
addtHeader map[string]struct{}
trustedOrigins map[string]struct{}
safeEndpoints map[string]struct{}
expectedOK bool
expectedStatus int
}{
{
name: "base case",
getCfg: func() *setting.Cfg {
return setting.NewCfg()
},
request: postRequest(t, "", nil, true),
expectedOK: true,
},
{
name: "base with null origin header",
getCfg: func() *setting.Cfg {
return setting.NewCfg()
},
request: postRequest(t, "", map[string]string{"Origin": "null"}, true),
expectedStatus: http.StatusForbidden,
},
{
name: "grafana.org",
getCfg: func() *setting.Cfg {
return setting.NewCfg()
},
request: postRequest(t, "grafana.org", map[string]string{"Origin": "https://grafana.org"}, true),
expectedOK: true,
},
{
name: "grafana.org with X-Forwarded-Host",
getCfg: func() *setting.Cfg {
return setting.NewCfg()
},
request: postRequest(t, "grafana.localhost", map[string]string{"X-Forwarded-Host": "grafana.org", "Origin": "https://grafana.org"}, true),
expectedStatus: http.StatusForbidden,
},
{
name: "grafana.org with X-Forwarded-Host and header trusted",
getCfg: func() *setting.Cfg {
return setting.NewCfg()
},
request: postRequest(t, "grafana.localhost", map[string]string{"X-Forwarded-Host": "grafana.org", "Origin": "https://grafana.org"}, true),
addtHeader: map[string]struct{}{"X-Forwarded-Host": {}},
expectedOK: true,
},
{
name: "grafana.org from grafana.com",
getCfg: func() *setting.Cfg {
return setting.NewCfg()
},
request: postRequest(t, "grafana.org", map[string]string{"Origin": "https://grafana.com"}, true),
expectedStatus: http.StatusForbidden,
},
{
name: "grafana.org from grafana.com explicit trust for grafana.com",
getCfg: func() *setting.Cfg {
return setting.NewCfg()
},
request: postRequest(t, "grafana.org", map[string]string{"Origin": "https://grafana.com"}, true),
trustedOrigins: map[string]struct{}{"grafana.com": {}},
expectedOK: true,
},
{
name: "grafana.org from grafana.com with X-Forwarded-Host and header trusted",
getCfg: func() *setting.Cfg {
return setting.NewCfg()
},
request: postRequest(t, "grafana.localhost", map[string]string{"X-Forwarded-Host": "grafana.org", "Origin": "https://grafana.com"}, true),
addtHeader: map[string]struct{}{"X-Forwarded-Host": {}},
trustedOrigins: map[string]struct{}{"grafana.com": {}},
expectedOK: true,
},
{
name: "safe endpoint",
getCfg: func() *setting.Cfg {
return setting.NewCfg()
},
request: postRequest(t, "example.org/foo/bar", map[string]string{"Origin": "null"}, true),
safeEndpoints: map[string]struct{}{"foo/bar": {}},
expectedOK: true,
},
{
name: "grafana.org with X-Forwarded-Host; will skip csrf check if login cookie is not present; without login cookie, should return nil because login cookie is not present",
getCfg: func() *setting.Cfg {
cfg := setting.NewCfg()
cfg.SectionWithEnvOverrides("security").Key("csrf_always_check").SetValue("false")
return cfg
},
request: postRequest(t, "grafana.localhost", map[string]string{"X-Forwarded-Host": "grafana.org", "Origin": "https://grafana.org"}, false),
expectedOK: true,
},
{
name: "grafana.org with X-Forwarded-Host; will perform csrf check even if login cookie is not present, should return error because host name does not match origin",
getCfg: func() *setting.Cfg {
cfg := setting.NewCfg()
cfg.SectionWithEnvOverrides("security").Key("csrf_always_check").SetValue("true")
return cfg
},
request: postRequest(t, "grafana.localhost", map[string]string{"X-Forwarded-Host": "grafana.org", "Origin": "https://grafana.org"}, false),
expectedStatus: http.StatusForbidden,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
csrf := ProvideCSRFFilter(tc.getCfg())
csrf.trustedOrigins = tc.trustedOrigins
csrf.headers = tc.addtHeader
csrf.safeEndpoints = tc.safeEndpoints
csrf.cfg.LoginCookieName = "LoginCookie"
err := csrf.check(tc.request)
if tc.expectedOK {
require.NoError(t, err)
} else {
require.Error(t, err)
var actual *errorWithStatus
require.True(t, errors.As(err, &actual))
assert.EqualValues(t, tc.expectedStatus, actual.HTTPStatus)
}
})
}
}
func postRequest(t testing.TB, hostname string, headers map[string]string, withLoginCookie bool) *http.Request {
t.Helper()
urlParts := strings.SplitN(hostname, "/", 2)
path := "/"
if len(urlParts) == 2 {
path = urlParts[1]
}
r, err := http.NewRequest(http.MethodPost, path, nil)
require.NoError(t, err)
r.Host = urlParts[0]
if withLoginCookie {
r.AddCookie(&http.Cookie{
Name: "LoginCookie",
Value: "this should not be important",
})
}
for k, v := range headers {
r.Header.Set(k, v)
}
return r
}
func csrfScenario(t *testing.T, cookieName, method, origin, host string) *httptest.ResponseRecorder {
req, err := http.NewRequest(method, "/", nil)
if err != nil {
t.Fatal(err)
}
req.AddCookie(&http.Cookie{
Name: cookieName,
})
// Note: Not sure where host header populates req.Host, or how that works.
req.Host = host
req.Header.Set("HOST", host)
req.Header.Set("ORIGIN", origin)
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
})
rr := httptest.NewRecorder()
cfg := setting.NewCfg()
cfg.LoginCookieName = cookieName
service := ProvideCSRFFilter(cfg)
handler := service.Middleware()(testHandler)
handler.ServeHTTP(rr, req)
return rr
}
func TestProvideCSRFFilter(t *testing.T) {
t.Parallel()
tests := []struct {
getInput func() *setting.Cfg
expectedAlwaysCheck bool
}{
{
getInput: func() *setting.Cfg {
return setting.NewCfg()
},
// Should default to false when config value is not set.
expectedAlwaysCheck: false,
},
{
getInput: func() *setting.Cfg {
cfg := setting.NewCfg()
cfg.SectionWithEnvOverrides("security").Key("csrf_always_check").SetValue("false")
return cfg
},
// Should be false when config value is set to false.
expectedAlwaysCheck: false,
},
{
getInput: func() *setting.Cfg {
cfg := setting.NewCfg()
cfg.SectionWithEnvOverrides("security").Key("csrf_always_check").SetValue("true")
return cfg
},
// Should be true when config value is set to true.
expectedAlwaysCheck: true,
},
}
for _, tc := range tests {
csrf := ProvideCSRFFilter(tc.getInput())
assert.Equal(t, tc.expectedAlwaysCheck, csrf.alwaysCheck)
}
}