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 <build@mattermost.com>
Этот коммит содержится в:
Doug Lauder
2023-07-12 17:55:13 -04:00
коммит произвёл GitHub
родитель cc6de45bde
Коммит a8244e9d10
7 изменённых файлов: 48 добавлений и 8 удалений

Просмотреть файл

@@ -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 {

Просмотреть файл

@@ -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)

Просмотреть файл

@@ -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},

Просмотреть файл

@@ -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,

Просмотреть файл

@@ -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"])
})
}

Просмотреть файл

@@ -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")

Просмотреть файл

@@ -79,6 +79,20 @@ export default class LinkingLandingPage extends PureComponent<Props, State> {
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<Props, State> {
render() {
const isMobile = UserAgent.isMobile();
if (this.checkLandingPreferenceBrowser()) {
if (this.checkLandingPreferenceBrowser() || this.isEmbedded()) {
this.openInBrowser();
return null;
}