Skip to content

Commit db11638

Browse files
authored
feat: Open CSP for multi-tenant B2B Twake Chat (#4826)
We open the `frame-src` and `connect-src` CSP rules for B2B instances so users can reach their organization's multi-tenant Twake Chat endpoints. Fixes #4825
2 parents 7d6db15 + 07a623d commit db11638

2 files changed

Lines changed: 64 additions & 6 deletions

File tree

web/middlewares/secure.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -307,20 +307,32 @@ func (b cspBuilder) makeCSPHeader(header, cspAllowList string, sources []CSPSour
307307
}
308308
}
309309
}
310-
// Add matrix.{org_domain} to frame-src directive if present (for iframes).
311-
// OrgDomain is validated to contain only safe characters before being injected into the header.
312-
if header == "frame-src" && b.instance != nil && isSafeDomain(b.instance.OrgDomain) {
313-
headers = append(headers, "matrix."+b.instance.OrgDomain)
310+
// Add org related domains to frame-src directive if present (for iframes).
311+
// OrgDomain is validated to contain only safe characters before being
312+
// injected into the header.
313+
if header == "frame-src" && b.instance != nil {
314+
if isSafeDomain(b.instance.OrgDomain) {
315+
headers = append(headers, "matrix."+b.instance.OrgDomain)
316+
}
317+
318+
_, domain, found := strings.Cut(b.instance.Domain, ".")
319+
if found && isSafeDomain(domain) && isSafeDomain(b.instance.OrgID) {
320+
headers = append(headers, "chat-"+b.instance.OrgID+".tc-apps."+domain)
321+
headers = append(headers, "auto-login-"+b.instance.OrgID+".tc-apps."+domain)
322+
}
314323
}
315-
// Add api-login-{org_id}.{domain without prefix} to connect-src directive if present.
316-
// OrgID, OrgDomain, and domain are all validated before being injected into the header.
324+
// Add org related domains to connect-src directive if present.
325+
// OrgID, OrgDomain, and domain are all validated before being injected
326+
// into the header.
317327
if header == "connect-src" && b.instance != nil && isSafeDomain(b.instance.OrgID) {
318328
_, domain, found := strings.Cut(b.instance.Domain, ".")
319329
if found && isSafeDomain(domain) {
320330
orgDomain := b.instance.OrgID + "." + domain
321331
headers = append(headers, "api-login-"+orgDomain)
322332
headers = append(headers, orgDomain)
323333
headers = append(headers, "wss://"+orgDomain)
334+
headers = append(headers, "chat-"+b.instance.OrgID+".tc-apps."+domain)
335+
headers = append(headers, "auto-login-"+b.instance.OrgID+".tc-apps."+domain)
324336
}
325337
if isSafeDomain(b.instance.OrgDomain) {
326338
orgAltDomain := b.instance.OrgID + "." + b.instance.OrgDomain

web/middlewares/secure_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ func TestSecure(t *testing.T) {
154154

155155
csp := rec.Header().Get(echo.HeaderContentSecurityPolicy)
156156

157+
// No OrgID set: Twake Chat multi-tenant domains must NOT be present.
158+
assert.NotContains(t, csp, "tc-apps",
159+
"CSP should NOT contain tc-apps domains when OrgID is empty. Full CSP: %s", csp)
160+
157161
// Verify that matrix.example.com appears only once (in frame-src)
158162
count := strings.Count(csp, "matrix.example.com")
159163
assert.Equal(t, 1, count,
@@ -236,6 +240,8 @@ func TestSecure(t *testing.T) {
236240
apiLoginDomain := "api-login-myorg123.cozy.example.com"
237241
orgInstanceDomain := "myorg123.cozy.example.com"
238242
orgInstanceWSDomain := "wss://myorg123.cozy.example.com"
243+
chatDomain := "chat-myorg123.tc-apps.cozy.example.com"
244+
autoLoginDomain := "auto-login-myorg123.tc-apps.cozy.example.com"
239245

240246
// Verify that connect-src contains the api-login domain
241247
connectSrcIndex := strings.Index(csp, "connect-src ")
@@ -253,6 +259,25 @@ func TestSecure(t *testing.T) {
253259
"connect-src should contain %s. Found: %s", orgInstanceDomain, connectSrcContent)
254260
assert.Contains(t, connectSrcContent, orgInstanceWSDomain,
255261
"connect-src should contain %s. Found: %s", orgInstanceWSDomain, connectSrcContent)
262+
assert.Contains(t, connectSrcContent, chatDomain,
263+
"connect-src should contain %s. Found: %s", chatDomain, connectSrcContent)
264+
assert.Contains(t, connectSrcContent, autoLoginDomain,
265+
"connect-src should contain %s. Found: %s", autoLoginDomain, connectSrcContent)
266+
267+
// Verify that frame-src contains the Twake Chat domains.
268+
frameSrcIndex := strings.Index(csp, "frame-src ")
269+
assert.NotEqual(t, -1, frameSrcIndex,
270+
"frame-src should be present in CSP. Full CSP: %s", csp)
271+
272+
frameSrcEnd := strings.Index(csp[frameSrcIndex:], ";")
273+
assert.NotEqual(t, -1, frameSrcEnd,
274+
"frame-src should end with semicolon")
275+
276+
frameSrcContent := csp[frameSrcIndex : frameSrcIndex+frameSrcEnd]
277+
assert.Contains(t, frameSrcContent, chatDomain,
278+
"frame-src should contain %s. Found: %s", chatDomain, frameSrcContent)
279+
assert.Contains(t, frameSrcContent, autoLoginDomain,
280+
"frame-src should contain %s. Found: %s", autoLoginDomain, frameSrcContent)
256281

257282
// Verify that other directives do NOT contain the api-login domain
258283
otherDirectives := []string{
@@ -336,6 +361,8 @@ func TestSecure(t *testing.T) {
336361
csp := rec.Header().Get(echo.HeaderContentSecurityPolicy)
337362
expectedDomain := "myorg123.example.com"
338363
expectedWSDomain := "wss://myorg123.example.com"
364+
chatDomain := "chat-myorg123.tc-apps.cozy.example.com"
365+
autoLoginDomain := "auto-login-myorg123.tc-apps.cozy.example.com"
339366

340367
count := strings.Count(csp, expectedDomain)
341368
assert.Equal(t, 3, count,
@@ -355,6 +382,25 @@ func TestSecure(t *testing.T) {
355382
"connect-src should contain %s. Found: %s", expectedDomain, connectSrcContent)
356383
assert.Contains(t, connectSrcContent, expectedWSDomain,
357384
"connect-src should contain %s. Found: %s", expectedWSDomain, connectSrcContent)
385+
assert.Contains(t, connectSrcContent, chatDomain,
386+
"connect-src should contain %s. Found: %s", chatDomain, connectSrcContent)
387+
assert.Contains(t, connectSrcContent, autoLoginDomain,
388+
"connect-src should contain %s. Found: %s", autoLoginDomain, connectSrcContent)
389+
390+
// Verify that frame-src contains the Twake Chat domains.
391+
frameSrcIndex := strings.Index(csp, "frame-src ")
392+
assert.NotEqual(t, -1, frameSrcIndex,
393+
"frame-src should be present in CSP. Full CSP: %s", csp)
394+
395+
frameSrcEnd := strings.Index(csp[frameSrcIndex:], ";")
396+
assert.NotEqual(t, -1, frameSrcEnd,
397+
"frame-src should end with semicolon")
398+
399+
frameSrcContent := csp[frameSrcIndex : frameSrcIndex+frameSrcEnd]
400+
assert.Contains(t, frameSrcContent, chatDomain,
401+
"frame-src should contain %s. Found: %s", chatDomain, frameSrcContent)
402+
assert.Contains(t, frameSrcContent, autoLoginDomain,
403+
"frame-src should contain %s. Found: %s", autoLoginDomain, frameSrcContent)
358404

359405
// Verify that img-src also contains the org domain
360406
imgSrcIndex := strings.Index(csp, "img-src ")

0 commit comments

Comments
 (0)