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>
Этот коммит содержится в:
@@ -2241,6 +2241,10 @@ func attachDeviceId(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
Secure: secure,
|
Secure: secure,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if secure && utils.CheckEmbeddedCookie(r) {
|
||||||
|
sessionCookie.SameSite = http.SameSiteNoneMode
|
||||||
|
}
|
||||||
|
|
||||||
http.SetCookie(w, sessionCookie)
|
http.SetCookie(w, sessionCookie)
|
||||||
|
|
||||||
if err := c.App.AttachDeviceId(c.AppContext.Session().Id, deviceId, c.AppContext.Session().ExpiresAt); err != nil {
|
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,
|
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, sessionCookie)
|
||||||
http.SetCookie(w, userCookie)
|
http.SetCookie(w, userCookie)
|
||||||
http.SetCookie(w, csrfCookie)
|
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) {
|
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{
|
RenderWebError(config, w, r, err.StatusCode, url.Values{
|
||||||
"message": []string{err.Message},
|
"message": []string{err.Message},
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ import (
|
|||||||
"github.com/mattermost/mattermost/server/v8/platform/services/tracing"
|
"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 {
|
func GetHandlerName(h func(*Context, http.ResponseWriter, *http.Request)) string {
|
||||||
handlerName := runtime.FuncForPC(reflect.ValueOf(h).Pointer()).Name()
|
handlerName := runtime.FuncForPC(reflect.ValueOf(h).Pointer()).Name()
|
||||||
pos := strings.LastIndex(handlerName, ".")
|
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.
|
// 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(
|
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,
|
cloudCSP,
|
||||||
h.cspShaDirective,
|
h.cspShaDirective,
|
||||||
devCSP,
|
devCSP,
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ func TestHandlerServeCSPHeader(t *testing.T) {
|
|||||||
response := httptest.NewRecorder()
|
response := httptest.NewRecorder()
|
||||||
handler.ServeHTTP(response, request)
|
handler.ServeHTTP(response, request)
|
||||||
assert.Equal(t, 200, response.Code)
|
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) {
|
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()
|
response := httptest.NewRecorder()
|
||||||
handler.ServeHTTP(response, request)
|
handler.ServeHTTP(response, request)
|
||||||
assert.Equal(t, 200, response.Code)
|
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) {
|
t.Run("static, with subpath", func(t *testing.T) {
|
||||||
@@ -404,7 +404,7 @@ func TestHandlerServeCSPHeader(t *testing.T) {
|
|||||||
response := httptest.NewRecorder()
|
response := httptest.NewRecorder()
|
||||||
handler.ServeHTTP(response, request)
|
handler.ServeHTTP(response, request)
|
||||||
assert.Equal(t, 200, response.Code)
|
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
|
// 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
|
// 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()
|
response = httptest.NewRecorder()
|
||||||
handler.ServeHTTP(response, request)
|
handler.ServeHTTP(response, request)
|
||||||
assert.Equal(t, 200, response.Code)
|
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.
|
// 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")
|
// 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()
|
response := httptest.NewRecorder()
|
||||||
handler.ServeHTTP(response, request)
|
handler.ServeHTTP(response, request)
|
||||||
assert.Equal(t, 200, response.Code)
|
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 (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"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("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("Content-Type", "text/html; charset=utf-8")
|
||||||
w.Header().Set("Cache-Control", "no-cache, max-age=31556926")
|
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;
|
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 = () => {
|
checkLandingPreferenceApp = () => {
|
||||||
const landingPreference = BrowserStore.getLandingPreference(this.props.siteUrl);
|
const landingPreference = BrowserStore.getLandingPreference(this.props.siteUrl);
|
||||||
return landingPreference && landingPreference === LandingPreferenceTypes.MATTERMOSTAPP;
|
return landingPreference && landingPreference === LandingPreferenceTypes.MATTERMOSTAPP;
|
||||||
@@ -435,7 +449,7 @@ export default class LinkingLandingPage extends PureComponent<Props, State> {
|
|||||||
render() {
|
render() {
|
||||||
const isMobile = UserAgent.isMobile();
|
const isMobile = UserAgent.isMobile();
|
||||||
|
|
||||||
if (this.checkLandingPreferenceBrowser()) {
|
if (this.checkLandingPreferenceBrowser() || this.isEmbedded()) {
|
||||||
this.openInBrowser();
|
this.openInBrowser();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user