From 942cd6f0bff750664fcd056405e4e67d561d7e78 Mon Sep 17 00:00:00 2001 From: Carlos Date: Mon, 19 Jan 2026 11:45:38 -0800 Subject: [PATCH 1/2] security: add Origin header validation for CSRF protection Validate Origin header on state-changing requests (POST, PUT, DELETE) to provide additional CSRF protection beyond SameSite cookies. - Add isOriginAllowed() function to validate request origins - Reject requests with invalid/disallowed Origin headers on POST/PUT/DELETE - Allow localhost origins in development mode - Log rejected requests for security monitoring - Dynamic CORS header based on request origin This complements SameSite=Lax cookies for comprehensive CSRF protection. Co-Authored-By: Claude Opus 4.5 --- backend/controllers/app_handlers.go | 40 ++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/backend/controllers/app_handlers.go b/backend/controllers/app_handlers.go index 29ba4300..684ec02d 100644 --- a/backend/controllers/app_handlers.go +++ b/backend/controllers/app_handlers.go @@ -6,6 +6,7 @@ import ( "encoding/json" "net/http" "os" + "strings" "github.com/gorilla/sessions" "golang.org/x/oauth2" @@ -109,13 +110,50 @@ func (a *App) UserInfoHandler(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(userInfo) } +// isOriginAllowed checks if the request origin is allowed +func isOriginAllowed(origin, allowedOrigin string) bool { + if origin == "" { + // No origin header - could be same-origin or non-browser client + return true + } + if allowedOrigin != "" && origin == allowedOrigin { + return true + } + // In development, allow localhost origins + if os.Getenv("ENV") != "production" { + if strings.HasPrefix(origin, "http://localhost") || + strings.HasPrefix(origin, "http://127.0.0.1") { + return true + } + } + return false +} + func (a *App) EnableCORS(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { allowedOrigin := os.Getenv("FRONTEND_ORIGIN_DEV") // frontend origin - w.Header().Set("Access-Control-Allow-Origin", allowedOrigin) + requestOrigin := r.Header.Get("Origin") + + // For state-changing requests (POST, PUT, DELETE), validate Origin header + // This provides additional CSRF protection beyond SameSite cookies + if r.Method == http.MethodPost || r.Method == http.MethodPut || r.Method == http.MethodDelete { + if !isOriginAllowed(requestOrigin, allowedOrigin) { + utils.Logger.Warnf("CSRF protection: rejected request from origin: %s", requestOrigin) + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + } + + // Set CORS headers + if requestOrigin != "" && isOriginAllowed(requestOrigin, allowedOrigin) { + w.Header().Set("Access-Control-Allow-Origin", requestOrigin) + } else if allowedOrigin != "" { + w.Header().Set("Access-Control-Allow-Origin", allowedOrigin) + } w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-User-Email, X-Encryption-Secret, X-User-UUID") w.Header().Set("Access-Control-Allow-Credentials", "true") // to allow credentials + if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return From 3aa9f8716b50135665c4987b29aa7f1e5edecc1e Mon Sep 17 00:00:00 2001 From: Carlos Date: Mon, 19 Jan 2026 13:08:41 -0800 Subject: [PATCH 2/2] Add Vary: Origin header to prevent caching issues Addresses review feedback to add the Vary header when responses differ based on the Origin header. This prevents browsers and CDNs from incorrectly caching CORS responses. Co-Authored-By: Claude Opus 4.5 --- backend/controllers/app_handlers.go | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/controllers/app_handlers.go b/backend/controllers/app_handlers.go index 684ec02d..7e8f7344 100644 --- a/backend/controllers/app_handlers.go +++ b/backend/controllers/app_handlers.go @@ -153,6 +153,7 @@ func (a *App) EnableCORS(handler http.Handler) http.Handler { w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-User-Email, X-Encryption-Secret, X-User-UUID") w.Header().Set("Access-Control-Allow-Credentials", "true") // to allow credentials + w.Header().Add("Vary", "Origin") // prevent caching issues with different origins if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK)