Skip to content

Commit 7471381

Browse files
zatteoshepilov
authored andcommitted
feat: Add api-login domain to connect-src directive
1 parent bb6d3e6 commit 7471381

File tree

2 files changed

+68
-98
lines changed

2 files changed

+68
-98
lines changed

web/middlewares/secure.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -305,13 +305,11 @@ func (b cspBuilder) makeCSPHeader(header, cspAllowList string, sources []CSPSour
305305
if header == "frame-src" && b.instance != nil && b.instance.OrgDomain != "" {
306306
headers = append(headers, "matrix."+b.instance.OrgDomain)
307307
}
308-
// Add api-login.{org_id}.{domain without prefix} to connect-src directive if present
308+
// Add api-login-{org_id}.{domain without prefix} to connect-src directive if present
309309
if header == "connect-src" && b.instance != nil && b.instance.OrgID != "" {
310-
if parts := strings.Split(b.instance.Domain, "."); len(parts) >= 3 {
311-
domainWithoutPrefix := strings.Join(parts[1:], ".")
312-
headers = append(headers, "api-login-"+b.instance.OrgID+"."+domainWithoutPrefix)
313-
} else {
314-
headers = append(headers, "api-login-"+b.instance.OrgID+"."+b.instance.Domain)
310+
_, domain, found := strings.Cut(b.instance.Domain, ".")
311+
if found {
312+
headers = append(headers, "api-login-"+b.instance.OrgID+"."+domain)
315313
}
316314
}
317315
if len(headers) == 0 {

web/middlewares/secure_test.go

Lines changed: 64 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -203,111 +203,83 @@ func TestSecure(t *testing.T) {
203203
}
204204
})
205205

206-
t.Run("SecureMiddlewareCSPWithOrgDomainAPILogin", func(t *testing.T) {
207-
// Test case 1: Domain with 3+ parts (alice.twake.app)
208-
// Should strip the first part and use "twake.app"
209-
e1 := echo.New()
210-
req1, _ := http.NewRequest(echo.GET, "http://alice.twake.app/", nil)
211-
rec1 := httptest.NewRecorder()
212-
c1 := e1.NewContext(req1, rec1)
213-
inst1 := &instance.Instance{
214-
Domain: "alice.twake.app",
215-
OrgDomain: "example.com",
216-
OrgID: "org123",
206+
t.Run("SecureMiddlewareCSPWithOrgID", func(t *testing.T) {
207+
e := echo.New()
208+
req, _ := http.NewRequest(echo.GET, "http://app.cozy.local/", nil)
209+
rec := httptest.NewRecorder()
210+
c := e.NewContext(req, rec)
211+
inst := &instance.Instance{
212+
Domain: "alice.cozy.example.com",
213+
OrgID: "myorg123",
217214
}
218-
c1.Set("instance", inst1)
219-
h1 := Secure(&SecureConfig{
220-
CSPConnectSrc: []CSPSource{CSPSrcSelf},
215+
c.Set("instance", inst)
216+
h := Secure(&SecureConfig{
217+
CSPDefaultSrc: []CSPSource{CSPSrcSelf},
218+
CSPScriptSrc: []CSPSource{CSPSrcSelf},
219+
CSPFrameSrc: []CSPSource{CSPSrcSelf},
220+
CSPConnectSrc: []CSPSource{CSPSrcSelf},
221+
CSPFontSrc: []CSPSource{CSPSrcSelf},
222+
CSPImgSrc: []CSPSource{CSPSrcSelf},
223+
CSPManifestSrc: []CSPSource{CSPSrcSelf},
224+
CSPMediaSrc: []CSPSource{CSPSrcSelf},
225+
CSPObjectSrc: []CSPSource{CSPSrcSelf},
226+
CSPStyleSrc: []CSPSource{CSPSrcSelf},
227+
CSPWorkerSrc: []CSPSource{CSPSrcSelf},
228+
CSPFrameAncestors: []CSPSource{CSPSrcSelf},
229+
CSPBaseURI: []CSPSource{CSPSrcSelf},
230+
CSPFormAction: []CSPSource{CSPSrcSelf},
221231
})(echo.NotFoundHandler)
222-
_ = h1(c1)
232+
_ = h(c)
223233

224-
csp1 := rec1.Header().Get(echo.HeaderContentSecurityPolicy)
234+
csp := rec.Header().Get(echo.HeaderContentSecurityPolicy)
225235

226-
// Should contain api-login-org123.twake.app (domain without alice prefix)
227-
assert.Contains(t, csp1, "api-login-org123.twake.app",
228-
"connect-src should contain api-login-org123.twake.app for domain with 3+ parts. CSP: %s", csp1)
236+
// Verify that api-login-myorg123.cozy.example.com appears only once (in connect-src)
237+
expectedDomain := "api-login-myorg123.cozy.example.com"
238+
count := strings.Count(csp, expectedDomain)
239+
assert.Equal(t, 1, count,
240+
"%s should appear exactly once (in connect-src), but found %d times. CSP: %s",
241+
expectedDomain, count, csp)
229242

230-
connectSrcIndex := strings.Index(csp1, "connect-src ")
243+
// Verify that connect-src contains the api-login domain
244+
connectSrcIndex := strings.Index(csp, "connect-src ")
231245
assert.NotEqual(t, -1, connectSrcIndex,
232-
"connect-src should be present in CSP. Full CSP: %s", csp1)
246+
"connect-src should be present in CSP. Full CSP: %s", csp)
233247

234-
connectSrcEnd := strings.Index(csp1[connectSrcIndex:], ";")
248+
connectSrcEnd := strings.Index(csp[connectSrcIndex:], ";")
235249
assert.NotEqual(t, -1, connectSrcEnd,
236250
"connect-src should end with semicolon")
237251

238-
connectSrcContent := csp1[connectSrcIndex : connectSrcIndex+connectSrcEnd]
239-
assert.Contains(t, connectSrcContent, "api-login-org123.twake.app",
240-
"connect-src should contain api-login-org123.twake.app. Found: %s", connectSrcContent)
252+
connectSrcContent := csp[connectSrcIndex : connectSrcIndex+connectSrcEnd]
253+
assert.Contains(t, connectSrcContent, expectedDomain,
254+
"connect-src should contain %s. Found: %s", expectedDomain, connectSrcContent)
241255

242-
// Test case 2: Domain with fewer than 3 parts (cozy.local)
243-
// Should use the domain as-is
244-
e2 := echo.New()
245-
req2, _ := http.NewRequest(echo.GET, "http://cozy.local/", nil)
246-
rec2 := httptest.NewRecorder()
247-
c2 := e2.NewContext(req2, rec2)
248-
inst2 := &instance.Instance{
249-
Domain: "cozy.local",
250-
OrgDomain: "example.com",
251-
OrgID: "org456",
256+
// Verify that other directives do NOT contain the api-login domain
257+
otherDirectives := []string{
258+
"default-src",
259+
"script-src",
260+
"frame-src",
261+
"font-src",
262+
"img-src",
263+
"manifest-src",
264+
"media-src",
265+
"object-src",
266+
"style-src",
267+
"worker-src",
268+
"frame-ancestors",
269+
"base-uri",
270+
"form-action",
252271
}
253-
c2.Set("instance", inst2)
254-
h2 := Secure(&SecureConfig{
255-
CSPConnectSrc: []CSPSource{CSPSrcSelf},
256-
})(echo.NotFoundHandler)
257-
_ = h2(c2)
258-
259-
csp2 := rec2.Header().Get(echo.HeaderContentSecurityPolicy)
260-
261-
// Should contain api-login-org456.cozy.local (full domain used)
262-
assert.Contains(t, csp2, "api-login-org456.cozy.local",
263-
"connect-src should contain api-login-org456.cozy.local for domain with <3 parts. CSP: %s", csp2)
264-
265-
connectSrcIndex2 := strings.Index(csp2, "connect-src ")
266-
assert.NotEqual(t, -1, connectSrcIndex2,
267-
"connect-src should be present in CSP. Full CSP: %s", csp2)
268272

269-
connectSrcEnd2 := strings.Index(csp2[connectSrcIndex2:], ";")
270-
assert.NotEqual(t, -1, connectSrcEnd2,
271-
"connect-src should end with semicolon")
272-
273-
connectSrcContent2 := csp2[connectSrcIndex2 : connectSrcIndex2+connectSrcEnd2]
274-
assert.Contains(t, connectSrcContent2, "api-login-org456.cozy.local",
275-
"connect-src should contain api-login-org456.cozy.local. Found: %s", connectSrcContent2)
276-
277-
// Test case 3: Domain with 4 parts (bob.acme.twake.app)
278-
// Should strip only the first part and use "acme.twake.app"
279-
e3 := echo.New()
280-
req3, _ := http.NewRequest(echo.GET, "http://bob.acme.twake.app/", nil)
281-
rec3 := httptest.NewRecorder()
282-
c3 := e3.NewContext(req3, rec3)
283-
inst3 := &instance.Instance{
284-
Domain: "bob.acme.twake.app",
285-
OrgDomain: "example.org",
286-
OrgID: "org789",
273+
for _, directivePattern := range otherDirectives {
274+
directiveIndex := strings.Index(csp, directivePattern+" ")
275+
if directiveIndex != -1 {
276+
directiveEnd := strings.Index(csp[directiveIndex:], ";")
277+
if directiveEnd != -1 {
278+
directiveContent := csp[directiveIndex : directiveIndex+directiveEnd]
279+
assert.NotContains(t, directiveContent, expectedDomain,
280+
"Directive %s should NOT contain %s. Found: %s", directivePattern, expectedDomain, directiveContent)
281+
}
282+
}
287283
}
288-
c3.Set("instance", inst3)
289-
h3 := Secure(&SecureConfig{
290-
CSPConnectSrc: []CSPSource{CSPSrcSelf},
291-
})(echo.NotFoundHandler)
292-
_ = h3(c3)
293-
294-
csp3 := rec3.Header().Get(echo.HeaderContentSecurityPolicy)
295-
296-
// Should contain api-login-org789.acme.twake.app (domain without bob prefix)
297-
assert.Contains(t, csp3, "api-login-org789.acme.twake.app",
298-
"connect-src should contain api-login-org789.acme.twake.app for domain with 4 parts. CSP: %s", csp3)
299-
300-
// Verify it's in connect-src directive
301-
connectSrcIndex3 := strings.Index(csp3, "connect-src ")
302-
assert.NotEqual(t, -1, connectSrcIndex3,
303-
"connect-src should be present in CSP. Full CSP: %s", csp3)
304-
305-
connectSrcEnd3 := strings.Index(csp3[connectSrcIndex3:], ";")
306-
assert.NotEqual(t, -1, connectSrcEnd3,
307-
"connect-src should end with semicolon")
308-
309-
connectSrcContent3 := csp3[connectSrcIndex3 : connectSrcIndex3+connectSrcEnd3]
310-
assert.Contains(t, connectSrcContent3, "api-login-org789.acme.twake.app",
311-
"connect-src should contain api-login-org789.acme.twake.app. Found: %s", connectSrcContent3)
312284
})
313285
}

0 commit comments

Comments
 (0)