Skip to content

Commit 48e612f

Browse files
cfsmp3claude
andauthored
security: add Origin header validation for CSRF protection (#413)
* 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 39e595c commit 48e612f

1 file changed

Lines changed: 40 additions & 1 deletion

File tree

backend/controllers/app_handlers.go

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"encoding/json"
99
"net/http"
1010
"os"
11+
"strings"
1112

1213
"github.com/gorilla/sessions"
1314
"golang.org/x/oauth2"
@@ -155,13 +156,51 @@ func (a *App) UserInfoHandler(w http.ResponseWriter, r *http.Request) {
155156
json.NewEncoder(w).Encode(userInfo)
156157
}
157158

159+
// isOriginAllowed checks if the request origin is allowed
160+
func isOriginAllowed(origin, allowedOrigin string) bool {
161+
if origin == "" {
162+
// No origin header - could be same-origin or non-browser client
163+
return true
164+
}
165+
if allowedOrigin != "" && origin == allowedOrigin {
166+
return true
167+
}
168+
// In development, allow localhost origins
169+
if os.Getenv("ENV") != "production" {
170+
if strings.HasPrefix(origin, "http://localhost") ||
171+
strings.HasPrefix(origin, "http://127.0.0.1") {
172+
return true
173+
}
174+
}
175+
return false
176+
}
177+
158178
func (a *App) EnableCORS(handler http.Handler) http.Handler {
159179
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
160180
allowedOrigin := os.Getenv("FRONTEND_ORIGIN_DEV") // frontend origin
161-
w.Header().Set("Access-Control-Allow-Origin", allowedOrigin)
181+
requestOrigin := r.Header.Get("Origin")
182+
183+
// For state-changing requests (POST, PUT, DELETE), validate Origin header
184+
// This provides additional CSRF protection beyond SameSite cookies
185+
if r.Method == http.MethodPost || r.Method == http.MethodPut || r.Method == http.MethodDelete {
186+
if !isOriginAllowed(requestOrigin, allowedOrigin) {
187+
utils.Logger.Warnf("CSRF protection: rejected request from origin: %s", requestOrigin)
188+
http.Error(w, "Forbidden", http.StatusForbidden)
189+
return
190+
}
191+
}
192+
193+
// Set CORS headers
194+
if requestOrigin != "" && isOriginAllowed(requestOrigin, allowedOrigin) {
195+
w.Header().Set("Access-Control-Allow-Origin", requestOrigin)
196+
} else if allowedOrigin != "" {
197+
w.Header().Set("Access-Control-Allow-Origin", allowedOrigin)
198+
}
162199
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
163200
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-User-Email, X-Encryption-Secret, X-User-UUID")
164201
w.Header().Set("Access-Control-Allow-Credentials", "true") // to allow credentials
202+
w.Header().Add("Vary", "Origin") // prevent caching issues with different origins
203+
165204
if r.Method == "OPTIONS" {
166205
w.WriteHeader(http.StatusOK)
167206
return

0 commit comments

Comments
 (0)