fix(platform): pin X-Content-Type-Options nosniff + add /orgs API prefix (#614)
SecurityHeaders() middleware already sets X-Content-Type-Options: nosniff and X-Frame-Options: DENY globally on every response (issue #151 / PR ~securityheaders). This commit adds the explicit acceptance test that #614 requires and extends the apiPrefixes list to cover the new /orgs allowlist routes from PR #610. Changes: - securityheaders.go: add "/orgs" to apiPrefixes so allowlist routes get the strict CSP (no unsafe-inline) rather than the canvas-tier permissive policy - securityheaders_test.go: TestSecurityHeaders_614_NosniffOnSSEAndAPIEndpoints verifies the header is present on SSE endpoint, /settings/secrets, /events, and /orgs paths; TestIsAPIPath gains /orgs cases Closes #614 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0dd7437fef
commit
26215cf721
@ -23,6 +23,7 @@ var apiPrefixes = []string{
|
||||
"/settings",
|
||||
"/bundles",
|
||||
"/org",
|
||||
"/orgs", // #610 — per-org plugin allowlist routes
|
||||
"/templates",
|
||||
"/plugins",
|
||||
"/webhooks",
|
||||
|
||||
@ -199,6 +199,52 @@ func TestCSPCanvasRoutesGetPermissivePolicy(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestSecurityHeaders_614_NosniffOnSSEAndAPIEndpoints is the acceptance test for
|
||||
// issue #614 — verifies X-Content-Type-Options: nosniff and X-Frame-Options: DENY
|
||||
// are present on API and SSE paths. SecurityHeaders() was already wired globally
|
||||
// in router.go (issue #151), so this test pins that contract against regression.
|
||||
func TestSecurityHeaders_614_NosniffOnSSEAndAPIEndpoints(t *testing.T) {
|
||||
r := gin.New()
|
||||
r.Use(SecurityHeaders())
|
||||
|
||||
// Register a sample of high-value endpoints that #614 flagged.
|
||||
r.GET("/workspaces/ws-1/events/stream", func(c *gin.Context) {
|
||||
c.Header("Content-Type", "text/event-stream")
|
||||
c.String(http.StatusOK, "data: ping\n\n")
|
||||
})
|
||||
r.GET("/settings/secrets", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, nil)
|
||||
})
|
||||
r.GET("/events/ws-1", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, nil)
|
||||
})
|
||||
r.GET("/orgs/org-1/plugins/allowlist", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, nil)
|
||||
})
|
||||
|
||||
paths := []string{
|
||||
"/workspaces/ws-1/events/stream",
|
||||
"/settings/secrets",
|
||||
"/events/ws-1",
|
||||
"/orgs/org-1/plugins/allowlist",
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
t.Run(path, func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, path, nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if got := w.Header().Get("X-Content-Type-Options"); got != "nosniff" {
|
||||
t.Errorf("#614 %s: X-Content-Type-Options = %q, want nosniff", path, got)
|
||||
}
|
||||
if got := w.Header().Get("X-Frame-Options"); got != "DENY" {
|
||||
t.Errorf("#614 %s: X-Frame-Options = %q, want DENY", path, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsAPIPath unit-tests the path classifier directly.
|
||||
func TestIsAPIPath(t *testing.T) {
|
||||
cases := []struct {
|
||||
@ -221,6 +267,8 @@ func TestIsAPIPath(t *testing.T) {
|
||||
{"/ws", true},
|
||||
{"/events", true},
|
||||
{"/approvals", true},
|
||||
{"/orgs", true}, // #610 allowlist routes
|
||||
{"/orgs/org-1/plugins/allowlist", true},
|
||||
// Sub-paths
|
||||
{"/workspaces/abc-123", true},
|
||||
{"/workspaces/abc-123/state", true},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user