From 8cf7841f2030d8cf140f0084089cf24f5ae08be6 Mon Sep 17 00:00:00 2001 From: Christian Galo Date: Mon, 28 Apr 2025 20:26:17 -0500 Subject: [PATCH] Add CSRF middleware implementation and update go.mod/go.sum for dependencies --- go.mod | 5 +- go.sum | 2 + internal/middleware/csrf.go | 213 ++++++++++++++++++++++++++++++++ internal/middleware/security.go | 21 ---- 4 files changed, 219 insertions(+), 22 deletions(-) create mode 100644 internal/middleware/csrf.go diff --git a/go.mod b/go.mod index 2034a7d..47e9fff 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,10 @@ require ( github.com/spf13/viper v1.19.0 ) -require github.com/rs/cors v1.11.1 // indirect +require ( + github.com/gorilla/csrf v1.7.3 // indirect + github.com/rs/cors v1.11.1 // indirect +) require ( github.com/coreos/go-oidc/v3 v3.12.0 diff --git a/go.sum b/go.sum index da4a4d9..0ec28fe 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/csrf v1.7.3 h1:BHWt6FTLZAb2HtWT5KDBf6qgpZzvtbp9QWDRKZMXJC0= +github.com/gorilla/csrf v1.7.3/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= diff --git a/internal/middleware/csrf.go b/internal/middleware/csrf.go new file mode 100644 index 0000000..5280e37 --- /dev/null +++ b/internal/middleware/csrf.go @@ -0,0 +1,213 @@ +package middleware + +import ( + "html/template" + "net/http" + + "github.com/gorilla/csrf" +) + +// CSRFConfig defines the configuration options for CSRF middleware +type CSRFConfig struct { + // Secret is the 32-byte secret key used to generate tokens + Secret []byte + + // Cookie defines cookie options + Cookie struct { + // Name of the CSRF cookie + Name string + + // Path where the cookie is valid + Path string + + // Domain where the cookie is valid + Domain string + + // MaxAge in seconds for the CSRF cookie + MaxAge int + + // Secure sets the Secure flag on the cookie + Secure bool + + // HttpOnly sets the HttpOnly flag on the cookie + HttpOnly bool + + // SameSite sets the SameSite attribute on the cookie + SameSite csrf.SameSiteMode + } + + // ErrorHandler to call when CSRF validation fails + ErrorHandler http.Handler + + // FieldName is the name of the hidden input field used by frontend + FieldName string + + // RequestHeader is the name of header used in AJAX requests + RequestHeader string + + // TrustedOrigins lists additional origins that are trusted + TrustedOrigins []string + + // Path defines URL paths where CSRF protection applies + // If empty, all paths are protected + Path string + + // Ignore functions determine if a request should skip CSRF protection + Ignore []func(r *http.Request) bool +} + +// DefaultCSRFConfig returns a default configuration for CSRF middleware +func DefaultCSRFConfig() CSRFConfig { + config := CSRFConfig{ + Secret: nil, // Must be set by the application + FieldName: "gorilla.csrf.Token", + RequestHeader: "X-CSRF-Token", + Path: "", + Ignore: []func(r *http.Request) bool{}, + } + + config.Cookie.Name = "_csrf" + config.Cookie.Path = "/" + config.Cookie.MaxAge = 86400 // 24 hours + config.Cookie.Secure = true + config.Cookie.HttpOnly = true + config.Cookie.SameSite = csrf.SameSiteStrictMode + + return config +} + +// CSRF middleware provides Cross-Site Request Forgery protection +func CSRF(config CSRFConfig) Middleware { + options := []csrf.Option{ + csrf.CookieName(config.Cookie.Name), + csrf.Path(config.Cookie.Path), + csrf.MaxAge(config.Cookie.MaxAge), + csrf.FieldName(config.FieldName), + csrf.RequestHeader(config.RequestHeader), + csrf.Secure(config.Cookie.Secure), + csrf.HttpOnly(config.Cookie.HttpOnly), + csrf.SameSite(config.Cookie.SameSite), + } + + if config.Cookie.Domain != "" { + options = append(options, csrf.Domain(config.Cookie.Domain)) + } + + if config.ErrorHandler != nil { + options = append(options, csrf.ErrorHandler(config.ErrorHandler)) + } + + if len(config.TrustedOrigins) > 0 { + options = append(options, csrf.TrustedOrigins(config.TrustedOrigins)) + } + + // Create CSRF protection middleware + csrfHandler := csrf.Protect(config.Secret, options...) + + return func(next http.Handler) http.Handler { + // Handle protection path + if config.Path != "" { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == config.Path || (len(r.URL.Path) >= len(config.Path) && + r.URL.Path[:len(config.Path)] == config.Path) { + // Check if the request should be ignored + for _, ignoreFunc := range config.Ignore { + if ignoreFunc(r) { + next.ServeHTTP(w, r) + return + } + } + csrfHandler(next).ServeHTTP(w, r) + return + } + next.ServeHTTP(w, r) + }) + } + + // Handle ignore functions + if len(config.Ignore) > 0 { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + for _, ignoreFunc := range config.Ignore { + if ignoreFunc(r) { + next.ServeHTTP(w, r) + return + } + } + csrfHandler(next).ServeHTTP(w, r) + }) + } + + // Apply CSRF to all routes if no path or ignores specified + return csrfHandler(next) + } +} + +// CSRFToken gets the CSRF token from the request context +func CSRFToken(r *http.Request) string { + return csrf.Token(r) +} + +// CSRFTemplateField gets the hidden input field containing the CSRF token +func CSRFTemplateField(r *http.Request) template.HTML { + return csrf.TemplateField(r) +} + +/* Usage example: + +1. Create a secure key for CSRF protection: + + key := make([]byte, 32) + _, err := rand.Read(key) + if err != nil { + log.Fatal(err) + } + +2. Create CSRF middleware with default configuration: + + csrfConfig := middleware.DefaultCSRFConfig() + csrfConfig.Secret = key + csrfMiddleware := middleware.CSRF(csrfConfig) + +3. Add the middleware to your routing (example with standard http): + + handler := middleware.CreateStack( + middleware.RequestID(), + middleware.Logging(logger), + middleware.CORS(corsConfig), + middleware.CSRF(csrfConfig), // Add CSRF protection + )(router) + +4. In your HTML templates, include the CSRF token in forms: + +
+ {{ .CSRFField }} + + +
+ + Where .CSRFField is provided to the template as: + + data := map[string]interface{}{ + "CSRFField": middleware.CSRFTemplateField(r), + } + +5. For AJAX requests, include the CSRF token in the request header: + + // JavaScript example + fetch('/api/protected', { + method: 'POST', + headers: { + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content'), + }, + body: JSON.stringify(data) + }) + + // Include meta tag in your HTML: + // + + Where .CSRFToken is provided to the template as: + + data := map[string]interface{}{ + "CSRFToken": middleware.CSRFToken(r), + } +*/ \ No newline at end of file diff --git a/internal/middleware/security.go b/internal/middleware/security.go index 63f975f..b5b3fc5 100644 --- a/internal/middleware/security.go +++ b/internal/middleware/security.go @@ -2,8 +2,6 @@ package middleware import ( "net/http" - - "github.com/gorilla/sessions" ) // SecurityHeaders adds security and cache-control headers to all responses @@ -54,25 +52,6 @@ func SecureHeaders() Middleware { } } -// middleware/csrf.go -func CSRFMiddleware(store sessions.Store) Middleware { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method == "POST" { - session, _ := store.Get(r, "auth-session") - csrfToken := session.Values["csrf_token"].(string) - formToken := r.FormValue("_csrf") - - if csrfToken != formToken { - http.Error(w, "Invalid CSRF token", http.StatusForbidden) - return - } - } - next.ServeHTTP(w, r) - }) - } -} - // MaxBodySize limits the maximum size of request bodies // size parameter is in bytes func MaxBodySize(maxSize int64) Middleware {