From dfca6c211d50a6c816924fd05f374539d6d3ef07 Mon Sep 17 00:00:00 2001 From: Doug Lauder Date: Wed, 5 Mar 2025 18:01:43 -0500 Subject: [PATCH] MM-63327 Config setting for `ServiceSettings.FrameAncestors` (#30409) * Add Embedding page to system console, with single setting for Frame Ancestors --- server/channels/web/handlers.go | 8 +-- server/channels/web/handlers_test.go | 11 ++-- server/channels/web/oauth.go | 2 +- server/public/model/config.go | 5 ++ .../admin_console/admin_definition.tsx | 19 ++++++ .../__snapshots__/admin_sidebar.test.tsx.snap | 66 +++++++++++++++++++ webapp/channels/src/i18n/en.json | 4 ++ 7 files changed, 103 insertions(+), 12 deletions(-) diff --git a/server/channels/web/handlers.go b/server/channels/web/handlers.go index 9d3531f1e05..6314ce629f5 100644 --- a/server/channels/web/handlers.go +++ b/server/channels/web/handlers.go @@ -25,10 +25,6 @@ import ( "github.com/mattermost/mattermost/server/v8/channels/utils" ) -const ( - frameAncestors = "'self' teams.microsoft.com" -) - func GetHandlerName(h func(*Context, http.ResponseWriter, *http.Request)) string { handlerName := runtime.FuncForPC(reflect.ValueOf(h).Pointer()).Name() pos := strings.LastIndex(handlerName, ".") @@ -242,8 +238,8 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Set content security policy. This is also specified in the root.html of the webapp in a meta tag. w.Header().Set("Content-Security-Policy", fmt.Sprintf( - "frame-ancestors %s; script-src 'self' cdn.rudderlabs.com%s%s", - frameAncestors, + "frame-ancestors 'self' %s; script-src 'self' cdn.rudderlabs.com%s%s", + *c.App.Config().ServiceSettings.FrameAncestors, h.cspShaDirective, devCSP, )) diff --git a/server/channels/web/handlers_test.go b/server/channels/web/handlers_test.go index 37fe2b21b38..97a8722b853 100644 --- a/server/channels/web/handlers_test.go +++ b/server/channels/web/handlers_test.go @@ -343,10 +343,10 @@ func TestHandlerServeCSPHeader(t *testing.T) { response := httptest.NewRecorder() handler.ServeHTTP(response, request) assert.Equal(t, 200, response.Code) - assert.Equal(t, []string{"frame-ancestors " + frameAncestors + "; script-src 'self' cdn.rudderlabs.com"}, response.Header()["Content-Security-Policy"]) + assert.Equal(t, []string{"frame-ancestors 'self' " + *th.App.Config().ServiceSettings.FrameAncestors + "; script-src 'self' cdn.rudderlabs.com"}, response.Header()["Content-Security-Policy"]) }) - t.Run("static, with subpath", func(t *testing.T) { + t.Run("static, with subpath and frame ancestors", func(t *testing.T) { th := SetupWithStoreMock(t) defer th.TearDown() @@ -367,6 +367,7 @@ func TestHandlerServeCSPHeader(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SiteURL = *cfg.ServiceSettings.SiteURL + "/subpath" + *cfg.ServiceSettings.FrameAncestors = "teams.microsoft.com *.cloud.microsoft" }) web := New(th.Server) @@ -384,7 +385,7 @@ func TestHandlerServeCSPHeader(t *testing.T) { response := httptest.NewRecorder() handler.ServeHTTP(response, request) assert.Equal(t, 200, response.Code) - assert.Equal(t, []string{"frame-ancestors " + frameAncestors + "; script-src 'self' cdn.rudderlabs.com"}, response.Header()["Content-Security-Policy"]) + assert.Equal(t, []string{"frame-ancestors 'self' " + *th.App.Config().ServiceSettings.FrameAncestors + "; script-src 'self' cdn.rudderlabs.com"}, response.Header()["Content-Security-Policy"]) // TODO: It's hard to unit test this now that the CSP directive is effectively // decided in Setup(). Circle back to this in master once the memory store is @@ -399,7 +400,7 @@ func TestHandlerServeCSPHeader(t *testing.T) { response = httptest.NewRecorder() handler.ServeHTTP(response, request) assert.Equal(t, 200, response.Code) - assert.Equal(t, []string{"frame-ancestors " + frameAncestors + "; script-src 'self' cdn.rudderlabs.com"}, response.Header()["Content-Security-Policy"]) + assert.Equal(t, []string{"frame-ancestors 'self' " + *th.App.Config().ServiceSettings.FrameAncestors + "; script-src 'self' cdn.rudderlabs.com"}, response.Header()["Content-Security-Policy"]) // TODO: See above. // assert.Contains(t, response.Header()["Content-Security-Policy"], "frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com 'sha256-tPOjw+tkVs9axL78ZwGtYl975dtyPHB6LYKAO2R3gR4='", "csp header incorrectly changed after subpath changed") }) @@ -429,7 +430,7 @@ func TestHandlerServeCSPHeader(t *testing.T) { response := httptest.NewRecorder() handler.ServeHTTP(response, request) assert.Equal(t, 200, response.Code) - assert.Equal(t, []string{"frame-ancestors " + frameAncestors + "; script-src 'self' cdn.rudderlabs.com 'unsafe-eval' 'unsafe-inline'"}, response.Header()["Content-Security-Policy"]) + assert.Equal(t, []string{"frame-ancestors 'self' " + *th.App.Config().ServiceSettings.FrameAncestors + "; script-src 'self' cdn.rudderlabs.com 'unsafe-eval' 'unsafe-inline'"}, response.Header()["Content-Security-Policy"]) }) } diff --git a/server/channels/web/oauth.go b/server/channels/web/oauth.go index 077baf0c5a7..b0a22413286 100644 --- a/server/channels/web/oauth.go +++ b/server/channels/web/oauth.go @@ -195,7 +195,7 @@ func authorizeOAuthPage(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAudit("success") w.Header().Set("X-Frame-Options", "SAMEORIGIN") - w.Header().Set("Content-Security-Policy", fmt.Sprintf("frame-ancestors %s", frameAncestors)) + w.Header().Set("Content-Security-Policy", fmt.Sprintf("frame-ancestors 'self' %s", *c.App.Config().ServiceSettings.FrameAncestors)) w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "no-cache, max-age=31556926") diff --git a/server/public/model/config.go b/server/public/model/config.go index 43700a026cb..05ddb3114d8 100644 --- a/server/public/model/config.go +++ b/server/public/model/config.go @@ -440,6 +440,7 @@ type ServiceSettings struct { MaximumURLLength *int `access:"environment_file_storage,write_restrictable,cloud_restrictable"` ScheduledPosts *bool `access:"site_posts"` EnableWebHubChannelIteration *bool `access:"write_restrictable,cloud_restrictable"` // telemetry: none + FrameAncestors *string `access:"write_restrictable,cloud_restrictable"` // telemetry: none } var MattermostGiphySdkKey string @@ -967,6 +968,10 @@ func (s *ServiceSettings) SetDefaults(isUpdate bool) { if s.EnableWebHubChannelIteration == nil { s.EnableWebHubChannelIteration = NewPointer(false) } + + if s.FrameAncestors == nil { + s.FrameAncestors = NewPointer("") + } } type CacheSettings struct { diff --git a/webapp/channels/src/components/admin_console/admin_definition.tsx b/webapp/channels/src/components/admin_console/admin_definition.tsx index ac5f0fed6d1..8178c41fbbc 100644 --- a/webapp/channels/src/components/admin_console/admin_definition.tsx +++ b/webapp/channels/src/components/admin_console/admin_definition.tsx @@ -5833,6 +5833,25 @@ const AdminDefinition: AdminDefinitionType = { ], }, }, + embedding: { + url: 'integrations/embedding', + title: defineMessage({id: 'admin.sidebar.embedding', defaultMessage: 'Embedding'}), + isHidden: it.not(it.userHasReadPermissionOnResource(RESOURCE_KEYS.INTEGRATIONS.CORS)), + schema: { + id: 'EmbeddingSettings', + name: defineMessage({id: 'admin.integrations.embedding', defaultMessage: 'Embedding'}), + settings: [ + { + type: 'text', + key: 'ServiceSettings.FrameAncestors', + label: defineMessage({id: 'admin.customization.frameAncestorTitle', defaultMessage: 'Frame Ancestors:'}), + help_text: defineMessage({id: 'admin.customization.frameAncestorDesc', defaultMessage: 'Allows the Mattermost web client to be embedded in other websites. Enter a space-separated list of domains that are allowed to embed the Mattermost web client. Leave blank to disallow embedding.'}), + isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.INTEGRATIONS.CORS)), + }, + ], + }, + }, + }, }, compliance: { diff --git a/webapp/channels/src/components/admin_console/admin_sidebar/__snapshots__/admin_sidebar.test.tsx.snap b/webapp/channels/src/components/admin_console/admin_sidebar/__snapshots__/admin_sidebar.test.tsx.snap index 9bccb1107ce..961d99e17fe 100644 --- a/webapp/channels/src/components/admin_console/admin_sidebar/__snapshots__/admin_sidebar.test.tsx.snap +++ b/webapp/channels/src/components/admin_console/admin_sidebar/__snapshots__/admin_sidebar.test.tsx.snap @@ -1121,6 +1121,17 @@ exports[`components/AdminSidebar should match snapshot 1`] = ` /> } /> + + } + /> } /> + + } + /> } /> + + } + /> } /> + + } + /> } /> + + } + /> } /> + + } + />