From a8244e9d1078ab9725d131cfa5b02091673a8195 Mon Sep 17 00:00:00 2001 From: Doug Lauder Date: Wed, 12 Jul 2023 17:55:13 -0400 Subject: [PATCH] MM-53147 Support for embedding Mattermost in an MSTeams iframe (#23776) * add teams to allowed frame-ancestors * fix unit tests * set SameSite attribute for session cookie * further restrict ancestors * skip landing page if in iframe * Only set cookie SameSite=None if embedded in iframe * don't set MMEMBED cookie on landing page (check only) * fully parse MMEMBED cookie * add comment * more comments --------- Co-authored-by: Mattermost Build --- server/channels/api4/user.go | 4 ++++ server/channels/app/login.go | 6 ++++++ server/channels/utils/api.go | 10 ++++++++++ server/channels/web/handlers.go | 7 ++++++- server/channels/web/handlers_test.go | 10 +++++----- server/channels/web/oauth.go | 3 ++- .../linking_landing_page.tsx | 16 +++++++++++++++- 7 files changed, 48 insertions(+), 8 deletions(-) diff --git a/server/channels/api4/user.go b/server/channels/api4/user.go index 569f69fe84..356d87e059 100644 --- a/server/channels/api4/user.go +++ b/server/channels/api4/user.go @@ -2241,6 +2241,10 @@ func attachDeviceId(c *Context, w http.ResponseWriter, r *http.Request) { Secure: secure, } + if secure && utils.CheckEmbeddedCookie(r) { + sessionCookie.SameSite = http.SameSiteNoneMode + } + http.SetCookie(w, sessionCookie) if err := c.App.AttachDeviceId(c.AppContext.Session().Id, deviceId, c.AppContext.Session().ExpiresAt); err != nil { diff --git a/server/channels/app/login.go b/server/channels/app/login.go index af02284173..d7a97a68f4 100644 --- a/server/channels/app/login.go +++ b/server/channels/app/login.go @@ -323,6 +323,12 @@ func (a *App) AttachSessionCookies(c *request.Context, w http.ResponseWriter, r Secure: secure, } + if secure && utils.CheckEmbeddedCookie(r) { + sessionCookie.SameSite = http.SameSiteNoneMode + userCookie.SameSite = http.SameSiteNoneMode + csrfCookie.SameSite = http.SameSiteNoneMode + } + http.SetCookie(w, sessionCookie) http.SetCookie(w, userCookie) http.SetCookie(w, csrfCookie) diff --git a/server/channels/utils/api.go b/server/channels/utils/api.go index 8f1af8830d..af46f36aaa 100644 --- a/server/channels/utils/api.go +++ b/server/channels/utils/api.go @@ -41,6 +41,16 @@ func OriginChecker(allowedOrigins string) func(*http.Request) bool { } } +// CheckEmbeddedCookie returns true if the MMEMBED cookie is set to 1. +// MMEMBED is set via any plugin that facilitates Mattermost embedded in an iframe (e.g. mattermost-plugin-msteams-sync). +func CheckEmbeddedCookie(r *http.Request) bool { + cookie, err := r.Cookie("MMEMBED") + if err != nil { + return false + } + return cookie.Value == "1" +} + func RenderWebAppError(config *model.Config, w http.ResponseWriter, r *http.Request, err *model.AppError, s crypto.Signer) { RenderWebError(config, w, r, err.StatusCode, url.Values{ "message": []string{err.Message}, diff --git a/server/channels/web/handlers.go b/server/channels/web/handlers.go index 576dd77dbd..c1c01c9118 100644 --- a/server/channels/web/handlers.go +++ b/server/channels/web/handlers.go @@ -30,6 +30,10 @@ import ( "github.com/mattermost/mattermost/server/v8/platform/services/tracing" ) +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, ".") @@ -241,7 +245,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 'self'; script-src 'self' cdn.rudderlabs.com%s%s%s", + "frame-ancestors %s; script-src 'self' cdn.rudderlabs.com%s%s%s", + frameAncestors, cloudCSP, h.cspShaDirective, devCSP, diff --git a/server/channels/web/handlers_test.go b/server/channels/web/handlers_test.go index 7529003825..e8a2254b75 100644 --- a/server/channels/web/handlers_test.go +++ b/server/channels/web/handlers_test.go @@ -340,7 +340,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 'self'; script-src 'self' cdn.rudderlabs.com js.stripe.com/v3"}, response.Header()["Content-Security-Policy"]) + assert.Equal(t, []string{"frame-ancestors " + frameAncestors + "; script-src 'self' cdn.rudderlabs.com js.stripe.com/v3"}, response.Header()["Content-Security-Policy"]) }) t.Run("static, without subpath or SelfHostedPurchase, does not allow Stripe in CSP", func(t *testing.T) { @@ -363,7 +363,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 'self'; script-src 'self' cdn.rudderlabs.com"}, response.Header()["Content-Security-Policy"]) + assert.Equal(t, []string{"frame-ancestors " + frameAncestors + "; script-src 'self' cdn.rudderlabs.com"}, response.Header()["Content-Security-Policy"]) }) t.Run("static, with subpath", func(t *testing.T) { @@ -404,7 +404,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 'self'; script-src 'self' cdn.rudderlabs.com js.stripe.com/v3"}, response.Header()["Content-Security-Policy"]) + assert.Equal(t, []string{"frame-ancestors " + frameAncestors + "; script-src 'self' cdn.rudderlabs.com js.stripe.com/v3"}, 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 @@ -419,7 +419,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 'self'; script-src 'self' cdn.rudderlabs.com js.stripe.com/v3"}, response.Header()["Content-Security-Policy"]) + assert.Equal(t, []string{"frame-ancestors " + frameAncestors + "; script-src 'self' cdn.rudderlabs.com js.stripe.com/v3"}, 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") }) @@ -449,7 +449,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 'self'; script-src 'self' cdn.rudderlabs.com js.stripe.com/v3 'unsafe-eval' 'unsafe-inline'"}, response.Header()["Content-Security-Policy"]) + assert.Equal(t, []string{"frame-ancestors " + frameAncestors + "; script-src 'self' cdn.rudderlabs.com js.stripe.com/v3 'unsafe-eval' 'unsafe-inline'"}, response.Header()["Content-Security-Policy"]) }) } diff --git a/server/channels/web/oauth.go b/server/channels/web/oauth.go index 07f9525d10..edbc14cedd 100644 --- a/server/channels/web/oauth.go +++ b/server/channels/web/oauth.go @@ -5,6 +5,7 @@ package web import ( "encoding/json" + "fmt" "html" "net/http" "net/url" @@ -177,7 +178,7 @@ func authorizeOAuthPage(c *Context, w http.ResponseWriter, r *http.Request) { } w.Header().Set("X-Frame-Options", "SAMEORIGIN") - w.Header().Set("Content-Security-Policy", "frame-ancestors 'self'") + w.Header().Set("Content-Security-Policy", fmt.Sprintf("frame-ancestors %s", frameAncestors)) w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "no-cache, max-age=31556926") diff --git a/webapp/channels/src/components/linking_landing_page/linking_landing_page.tsx b/webapp/channels/src/components/linking_landing_page/linking_landing_page.tsx index f59c319a30..9e63dd0ad3 100644 --- a/webapp/channels/src/components/linking_landing_page/linking_landing_page.tsx +++ b/webapp/channels/src/components/linking_landing_page/linking_landing_page.tsx @@ -79,6 +79,20 @@ export default class LinkingLandingPage extends PureComponent { return landingPreference && landingPreference === LandingPreferenceTypes.BROWSER; }; + isEmbedded = () => { + // this cookie is set by any plugin that facilitates iframe embedding (e.g. mattermost-plugin-msteams-sync). + const cookieName = 'MMEMBED'; + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + if (cookie.startsWith(cookieName + '=')) { + const value = cookie.substring(cookieName.length + 1); + return decodeURIComponent(value) === '1'; + } + } + return false; + }; + checkLandingPreferenceApp = () => { const landingPreference = BrowserStore.getLandingPreference(this.props.siteUrl); return landingPreference && landingPreference === LandingPreferenceTypes.MATTERMOSTAPP; @@ -435,7 +449,7 @@ export default class LinkingLandingPage extends PureComponent { render() { const isMobile = UserAgent.isMobile(); - if (this.checkLandingPreferenceBrowser()) { + if (this.checkLandingPreferenceBrowser() || this.isEmbedded()) { this.openInBrowser(); return null; }