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