From cb37aa850c3f3c05a6e3012e38d77a9f0277535b Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Wed, 15 Apr 2026 16:52:19 -0700 Subject: [PATCH] fix(security): add Referrer-Policy + Permissions-Policy headers (#282) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #282. CLAUDE.md documented the SecurityHeaders() middleware as setting 6 headers (X-Content-Type-Options, X-Frame-Options, Referrer- Policy, Content-Security-Policy, Permissions-Policy, HSTS) but the implementation only set 4 — Referrer-Policy and Permissions-Policy were silently missing. Adds: - Referrer-Policy: strict-origin-when-cross-origin — prevents browsers from leaking full paths/queries in Referer on cross- origin navigation. Particularly relevant for canvas embeds of Langfuse trace URLs that may contain trace IDs. - Permissions-Policy: camera=(), microphone=(), geolocation=() — denies sensor access by default. Iframes the canvas embeds (Langfuse trace viewer etc.) can no longer request these without an explicit delegation. Regression tests added to securityheaders_test.go — both headers are now in the same table-driven assertion loop as the other 4, so a future edit that drops them again fails CI loudly. LOW severity — this is defense-in-depth, not a direct exploit path. Co-Authored-By: Claude Opus 4.6 (1M context) --- platform/internal/middleware/securityheaders.go | 10 ++++++++++ platform/internal/middleware/securityheaders_test.go | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/platform/internal/middleware/securityheaders.go b/platform/internal/middleware/securityheaders.go index 6e678897..ca98ef10 100644 --- a/platform/internal/middleware/securityheaders.go +++ b/platform/internal/middleware/securityheaders.go @@ -9,12 +9,22 @@ import "github.com/gin-gonic/gin" // - X-Frame-Options: DENY — blocks iframe embedding (clickjacking) // - Content-Security-Policy: default-src 'self' — restricts resource loading to same origin // - Strict-Transport-Security: max-age=31536000; includeSubDomains — enforces HTTPS for 1 year +// - Referrer-Policy: strict-origin-when-cross-origin — avoids leaking full paths/queries in Referer +// - Permissions-Policy: camera=(), microphone=(), geolocation=() — denies sensor access for embedded content func SecurityHeaders() gin.HandlerFunc { return func(c *gin.Context) { c.Header("X-Content-Type-Options", "nosniff") c.Header("X-Frame-Options", "DENY") c.Header("Content-Security-Policy", "default-src 'self'") c.Header("Strict-Transport-Security", "max-age=31536000; includeSubDomains") + // #282: these two were documented in CLAUDE.md but missing from + // the middleware. Referrer-Policy prevents browsers from leaking + // the full Referer URL to cross-origin resources (which can + // expose internal paths/queries). Permissions-Policy denies + // sensor access by default — especially relevant because the + // canvas embeds iframes for Langfuse traces. + c.Header("Referrer-Policy", "strict-origin-when-cross-origin") + c.Header("Permissions-Policy", "camera=(), microphone=(), geolocation=()") c.Next() } } diff --git a/platform/internal/middleware/securityheaders_test.go b/platform/internal/middleware/securityheaders_test.go index 0bb91880..ade6d7d8 100644 --- a/platform/internal/middleware/securityheaders_test.go +++ b/platform/internal/middleware/securityheaders_test.go @@ -35,6 +35,10 @@ func TestSecurityHeaders(t *testing.T) { {"X-Frame-Options", "DENY"}, {"Content-Security-Policy", "default-src 'self'"}, {"Strict-Transport-Security", "max-age=31536000; includeSubDomains"}, + // #282: regression guards for the two headers that were + // documented in CLAUDE.md but missing from the implementation. + {"Referrer-Policy", "strict-origin-when-cross-origin"}, + {"Permissions-Policy", "camera=(), microphone=(), geolocation=()"}, } for _, tt := range tests {