3 changed files with 80 additions and 3 deletions
@ -0,0 +1,52 @@ |
|||||
|
package middleware |
||||
|
|
||||
|
import ( |
||||
|
"crypto/rand" |
||||
|
"encoding/base64" |
||||
|
"net/http" |
||||
|
"strings" |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
// CSRFSimple is a lightweight double-submit-cookie CSRF middleware suitable for HTMX
|
||||
|
// - Sets a readable XSRF-TOKEN cookie on safe requests if missing
|
||||
|
// - Requires unsafe requests to include X-CSRF-Token header matching the cookie
|
||||
|
func CSRFSimple(next http.Handler) http.Handler { |
||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
||||
|
method := strings.ToUpper(r.Method) |
||||
|
// Determine if connection is effectively HTTPS (behind proxy aware)
|
||||
|
isHTTPS := r.TLS != nil || strings.EqualFold(r.Header.Get("X-Forwarded-Proto"), "https") |
||||
|
|
||||
|
// Ensure token cookie exists for safe methods
|
||||
|
if method == http.MethodGet || method == http.MethodHead || method == http.MethodOptions || method == http.MethodTrace { |
||||
|
if _, err := r.Cookie("XSRF-TOKEN"); err != nil { |
||||
|
// Generate a random token
|
||||
|
buf := make([]byte, 32) |
||||
|
if _, err := rand.Read(buf); err == nil { |
||||
|
token := base64.RawURLEncoding.EncodeToString(buf) |
||||
|
http.SetCookie(w, &http.Cookie{ |
||||
|
Name: "XSRF-TOKEN", |
||||
|
Value: token, |
||||
|
Path: "/", |
||||
|
HttpOnly: false, // must be readable by client script
|
||||
|
Secure: isHTTPS, |
||||
|
SameSite: http.SameSiteLaxMode, |
||||
|
Expires: time.Now().Add(12 * time.Hour), |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
next.ServeHTTP(w, r) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// For unsafe methods, require header matches cookie
|
||||
|
headerToken := r.Header.Get("X-CSRF-Token") |
||||
|
cookie, err := r.Cookie("XSRF-TOKEN") |
||||
|
if err != nil || headerToken == "" || cookie == nil || cookie.Value == "" || cookie.Value != headerToken { |
||||
|
http.Error(w, "Forbidden - CSRF", http.StatusForbidden) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
next.ServeHTTP(w, r) |
||||
|
}) |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
package middleware |
||||
|
|
||||
|
import ( |
||||
|
"net/http" |
||||
|
) |
||||
|
|
||||
|
func SecurityHeaders(next http.Handler) http.Handler { |
||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
||||
|
w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' https://unpkg.com 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'") |
||||
|
w.Header().Set("X-Frame-Options", "DENY") |
||||
|
w.Header().Set("X-Content-Type-Options", "nosniff") |
||||
|
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") |
||||
|
if r.TLS != nil { |
||||
|
w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains; preload") |
||||
|
} |
||||
|
next.ServeHTTP(w, r) |
||||
|
}) |
||||
|
} |
||||
Loading…
Reference in new issue