From fbdff595fea888ba6bfbab998b2fd04ba2eeb341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Tue, 21 Apr 2026 18:49:32 +0400 Subject: [PATCH] Fix CORS and CSRF for cookie banner POST /consents endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cookie banner's cross-origin POST was blocked by two layered issues: 1. The global cors.Handler (with OptionsPassthrough: false) intercepted OPTIONS preflights before the cookie banner's own CORS middleware could run. Customer website origins aren't in AllowedOrigins, so the preflight response had no CORS headers. Move the cookie banner mount outside the global CORS group since it handles CORS per-banner. 2. The CSRF bypass patterns used literal "*" instead of ServeMux wildcard syntax "{rest...}", so they never matched real request paths like POST /cookie-banner/v1/{bannerID}/consents. Also remove redundant GET/OPTIONS bypass patterns since safe methods are always allowed. Signed-off-by: Émile Ré --- pkg/server/api/api.go | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/pkg/server/api/api.go b/pkg/server/api/api.go index 36c4661ef..cc40b4366 100644 --- a/pkg/server/api/api.go +++ b/pkg/server/api/api.go @@ -141,9 +141,9 @@ func NewServer(cfg Config) (*Server, error) { // The cookie banner API is called cross-origin from customer websites // by the JS SDK. CORS is handled by the cookie banner middleware. - csrf.AddInsecureBypassPattern("GET /cookie-banner/v1/*") - csrf.AddInsecureBypassPattern("POST /cookie-banner/v1/*") - csrf.AddInsecureBypassPattern("OPTIONS /cookie-banner/v1/*") + // GET and OPTIONS are safe methods (always allowed), but we bypass + // POST explicitly since it comes from customer origins. + csrf.AddInsecureBypassPattern("POST /cookie-banner/v1/{rest...}") // OAuth2 token, introspection, revocation, and device authorization // endpoints receive cross-origin POSTs from external clients. @@ -258,15 +258,21 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { router.MethodNotAllowed(methodNotAllowed) router.NotFound(notFound) - router.Use(cors.Handler(corsOpts)) - - router.Mount("/console/v1", http.StripPrefix("/console/v1", s.consoleHandler)) - router.Mount("/connect/v1", http.StripPrefix("/connect/v1", s.connectHandler)) + // Cookie banner has its own per-banner CORS middleware; mount it + // outside the global CORS handler so OPTIONS preflights from + // customer websites are not swallowed by the stricter AllowedOrigins + // list that applies to console/connect routes. router.Mount("/cookie-banner/v1", http.StripPrefix("/cookie-banner/v1", s.cookieBannerHandler)) - router.Mount("/files/v1", http.StripPrefix("/files/v1", s.filesHandler)) - router.Mount("/trust/v1", http.StripPrefix("/trust/v1", s.compliancePageHandler)) - router.Mount("/mcp/v1", http.StripPrefix("/mcp/v1", s.mcpHandler)) - router.Mount("/slack/v1", http.StripPrefix("/slack/v1", s.slackHandler)) + + router.Group(func(r chi.Router) { + r.Use(cors.Handler(corsOpts)) + r.Mount("/console/v1", http.StripPrefix("/console/v1", s.consoleHandler)) + r.Mount("/connect/v1", http.StripPrefix("/connect/v1", s.connectHandler)) + r.Mount("/files/v1", http.StripPrefix("/files/v1", s.filesHandler)) + r.Mount("/trust/v1", http.StripPrefix("/trust/v1", s.compliancePageHandler)) + r.Mount("/mcp/v1", http.StripPrefix("/mcp/v1", s.mcpHandler)) + r.Mount("/slack/v1", http.StripPrefix("/slack/v1", s.slackHandler)) + }) s.csrf.Handler(router).ServeHTTP(w, r) }