diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 3fe69b911..54b0423c5 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -33,6 +33,13 @@ func (g DashboardHandler) Append(router *mux.Router) { Handler(http.StripPrefix("/dashboard/", http.FileServer(g.Assets))) } +func (g DashboardHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // allow iframes from our domains only + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-src + w.Header().Set("Content-Security-Policy", "frame-src 'self' https://traefik.io https://*.traefik.io;") + http.FileServer(g.Assets).ServeHTTP(w, r) +} + func safePrefix(req *http.Request) string { prefix := req.Header.Get("X-Forwarded-Prefix") if prefix == "" { diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index c945a7000..384071dfe 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -1,9 +1,12 @@ package api import ( + "fmt" "net/http" + "net/http/httptest" "testing" + assetfs "github.com/elazarl/go-bindata-assetfs" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -52,3 +55,70 @@ func Test_safePrefix(t *testing.T) { }) } } + +func Test_ContentSecurityPolicy(t *testing.T) { + testCases := []struct { + desc string + handler DashboardHandler + expected int + }{ + { + desc: "OK", + handler: DashboardHandler{ + Assets: &assetfs.AssetFS{ + Asset: func(path string) ([]byte, error) { + return []byte{}, nil + }, + AssetDir: func(path string) ([]string, error) { + return []string{}, nil + }, + }, + }, + expected: http.StatusOK, + }, + { + desc: "Not found", + handler: DashboardHandler{ + Assets: &assetfs.AssetFS{ + Asset: func(path string) ([]byte, error) { + return []byte{}, fmt.Errorf("not found") + }, + AssetDir: func(path string) ([]string, error) { + return []string{}, fmt.Errorf("not found") + }, + }, + }, + expected: http.StatusNotFound, + }, + { + desc: "Internal server error", + handler: DashboardHandler{ + Assets: &assetfs.AssetFS{ + Asset: func(path string) ([]byte, error) { + return []byte{}, fmt.Errorf("oops") + }, + AssetDir: func(path string) ([]string, error) { + return []string{}, fmt.Errorf("oops") + }, + }, + }, + expected: http.StatusInternalServerError, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + req := httptest.NewRequest(http.MethodGet, "/foobar.html", nil) + + rw := httptest.NewRecorder() + + test.handler.ServeHTTP(rw, req) + + assert.Equal(t, test.expected, rw.Code) + assert.Equal(t, "frame-src 'self' https://traefik.io https://*.traefik.io;", rw.Result().Header.Get("Content-Security-Policy")) + }) + } +} diff --git a/pkg/server/service/managerfactory.go b/pkg/server/service/managerfactory.go index 438c089b2..172f956f9 100644 --- a/pkg/server/service/managerfactory.go +++ b/pkg/server/service/managerfactory.go @@ -39,7 +39,7 @@ func NewManagerFactory(staticConfiguration static.Configuration, routinesPool *s factory.api = api.NewBuilder(staticConfiguration) if staticConfiguration.API.Dashboard { - factory.dashboardHandler = http.FileServer(staticConfiguration.API.DashboardAssets) + factory.dashboardHandler = api.DashboardHandler{Assets: staticConfiguration.API.DashboardAssets} } }