Skip to content

Commit ecb93a6

Browse files
authored
feat: Allow CSP per org_domain (#4642)
This change automatically adds *.org_domain to all Content Security Policy directives when an instance has an org_domain configured. This allows loading resources (such as chat applications) hosted on the organization's domain and its subdomains.
2 parents 4407994 + 94b8c3f commit ecb93a6

File tree

2 files changed

+93
-2
lines changed

2 files changed

+93
-2
lines changed

web/middlewares/secure.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"strings"
77
"time"
88

9+
"github.com/cozy/cozy-stack/model/instance"
910
build "github.com/cozy/cozy-stack/pkg/config"
1011
"github.com/cozy/cozy-stack/pkg/config/config"
1112
"github.com/labstack/echo/v4"
@@ -134,15 +135,20 @@ func Secure(conf *SecureConfig) echo.MiddlewareFunc {
134135
return err
135136
}
136137
var contextName string
137-
if conf.CSPPerContext != nil {
138-
contextName = GetInstance(c).ContextName
138+
var inst *instance.Instance
139+
inst, ok := GetInstanceSafe(c)
140+
if ok {
141+
if conf.CSPPerContext != nil {
142+
contextName = inst.ContextName
143+
}
139144
}
140145
b := cspBuilder{
141146
parent: parent,
142147
siblings: siblings,
143148
isSecure: isSecure,
144149
contextName: contextName,
145150
perContext: conf.CSPPerContext,
151+
instance: inst,
146152
}
147153
cspHeader += b.makeCSPHeader("default-src", conf.CSPDefaultSrcAllowList, conf.CSPDefaultSrc)
148154
cspHeader += b.makeCSPHeader("script-src", conf.CSPScriptSrcAllowList, conf.CSPScriptSrc)
@@ -227,6 +233,7 @@ type cspBuilder struct {
227233
siblings string
228234
contextName string
229235
perContext map[string]map[string]string
236+
instance *instance.Instance
230237
isSecure bool
231238
}
232239

@@ -294,6 +301,10 @@ func (b cspBuilder) makeCSPHeader(header, cspAllowList string, sources []CSPSour
294301
}
295302
}
296303
}
304+
// Add matrix.org_domain to frame-src directive if present (for iframes)
305+
if header == "frame-src" && b.instance != nil && b.instance.OrgDomain != "" {
306+
headers = append(headers, "matrix."+b.instance.OrgDomain)
307+
}
297308
if len(headers) == 0 {
298309
return ""
299310
}

web/middlewares/secure_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package middlewares
33
import (
44
"net/http"
55
"net/http/httptest"
6+
"strings"
67
"testing"
78
"time"
89

@@ -122,4 +123,83 @@ func TestSecure(t *testing.T) {
122123
r = appendCSPRule("script '*'; toto;", "frame-ancestors", "new-rule")
123124
assert.Equal(t, "script '*'; toto;frame-ancestors new-rule;", r)
124125
})
126+
127+
t.Run("SecureMiddlewareCSPWithOrgDomain", func(t *testing.T) {
128+
e := echo.New()
129+
req, _ := http.NewRequest(echo.GET, "http://app.cozy.local/", nil)
130+
rec := httptest.NewRecorder()
131+
c := e.NewContext(req, rec)
132+
inst := &instance.Instance{
133+
Domain: "cozy.local",
134+
OrgDomain: "example.com",
135+
}
136+
c.Set("instance", inst)
137+
h := Secure(&SecureConfig{
138+
CSPDefaultSrc: []CSPSource{CSPSrcSelf},
139+
CSPScriptSrc: []CSPSource{CSPSrcSelf},
140+
CSPFrameSrc: []CSPSource{CSPSrcSelf},
141+
CSPConnectSrc: []CSPSource{CSPSrcSelf},
142+
CSPFontSrc: []CSPSource{CSPSrcSelf},
143+
CSPImgSrc: []CSPSource{CSPSrcSelf},
144+
CSPManifestSrc: []CSPSource{CSPSrcSelf},
145+
CSPMediaSrc: []CSPSource{CSPSrcSelf},
146+
CSPObjectSrc: []CSPSource{CSPSrcSelf},
147+
CSPStyleSrc: []CSPSource{CSPSrcSelf},
148+
CSPWorkerSrc: []CSPSource{CSPSrcSelf},
149+
CSPFrameAncestors: []CSPSource{CSPSrcSelf},
150+
CSPBaseURI: []CSPSource{CSPSrcSelf},
151+
CSPFormAction: []CSPSource{CSPSrcSelf},
152+
})(echo.NotFoundHandler)
153+
_ = h(c)
154+
155+
csp := rec.Header().Get(echo.HeaderContentSecurityPolicy)
156+
157+
// Verify that matrix.example.com appears only once (in frame-src)
158+
count := strings.Count(csp, "matrix.example.com")
159+
assert.Equal(t, 1, count,
160+
"matrix.example.com should appear exactly once (in frame-src), but found %d times. CSP: %s",
161+
count, csp)
162+
163+
// Verify that frame-src contains matrix.example.com
164+
frameSrcIndex := strings.Index(csp, "frame-src ")
165+
assert.NotEqual(t, -1, frameSrcIndex,
166+
"frame-src should be present in CSP. Full CSP: %s", csp)
167+
168+
frameSrcEnd := strings.Index(csp[frameSrcIndex:], ";")
169+
assert.NotEqual(t, -1, frameSrcEnd,
170+
"frame-src should end with semicolon")
171+
172+
frameSrcContent := csp[frameSrcIndex : frameSrcIndex+frameSrcEnd]
173+
assert.Contains(t, frameSrcContent, "matrix.example.com",
174+
"frame-src should contain matrix.example.com. Found: %s", frameSrcContent)
175+
176+
// Verify that other directives do NOT contain matrix.example.com
177+
otherDirectives := []string{
178+
"default-src",
179+
"script-src",
180+
"connect-src",
181+
"font-src",
182+
"img-src",
183+
"manifest-src",
184+
"media-src",
185+
"object-src",
186+
"style-src",
187+
"worker-src",
188+
"frame-ancestors",
189+
"base-uri",
190+
"form-action",
191+
}
192+
193+
for _, directivePattern := range otherDirectives {
194+
directiveIndex := strings.Index(csp, directivePattern+" ")
195+
if directiveIndex != -1 {
196+
directiveEnd := strings.Index(csp[directiveIndex:], ";")
197+
if directiveEnd != -1 {
198+
directiveContent := csp[directiveIndex : directiveIndex+directiveEnd]
199+
assert.NotContains(t, directiveContent, "matrix.example.com",
200+
"Directive %s should NOT contain matrix.example.com. Found: %s", directivePattern, directiveContent)
201+
}
202+
}
203+
}
204+
})
125205
}

0 commit comments

Comments
 (0)