You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
52 lines
1.7 KiB
52 lines
1.7 KiB
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)
|
|
})
|
|
}
|
|
|