package httpapi // csrf.go — Double-Submit-Cookie CSRF-Schutz (K1) und neuteredFileSystem (N5). // // Jede sichere State-ändernde Anfrage (POST/PUT/PATCH/DELETE) muss: // 1. Den Cookie „morz_csrf" enthalten. // 2. Den gleichen Wert als Form-Feld „csrf_token" oder Header „X-CSRF-Token" mitsenden. // // Token-Erzeugung: beim Rendern der Login-/Manage-Seiten wird SetCSRFCookie aufgerufen. // Token-Validierung: CSRFProtect-Middleware prüft, ob Cookie und Payload übereinstimmen. // // SameSite=Lax schützt bereits gegen die meisten CSRF-Angriffe aus anderen Domains, // aber das Double-Submit-Pattern bietet zusätzlichen Schutz für Formulare die per GET // auf anderen Seiten eingebettet werden könnten. import ( "crypto/rand" "encoding/hex" "net/http" ) const ( csrfCookieName = "morz_csrf" csrfFieldName = "csrf_token" csrfHeaderName = "X-CSRF-Token" ) // GenerateCSRFToken erzeugt ein 32-Byte zufälliges Hex-Token. func GenerateCSRFToken() (string, error) { buf := make([]byte, 32) if _, err := rand.Read(buf); err != nil { return "", err } return hex.EncodeToString(buf), nil } // SetCSRFCookie setzt (oder erneuert) den CSRF-Cookie im Response. // Gibt das Token zurück, damit es in Template-Daten eingebettet werden kann. func SetCSRFCookie(w http.ResponseWriter, r *http.Request, devMode bool) string { // Existierendes Token wiederverwenden, wenn vorhanden. if c, err := r.Cookie(csrfCookieName); err == nil && c.Value != "" { return c.Value } token, err := GenerateCSRFToken() if err != nil { // Im Fehlerfall leeres Token — Handler müssen diesen Fall prüfen. return "" } http.SetCookie(w, &http.Cookie{ Name: csrfCookieName, Value: token, Path: "/", HttpOnly: false, // JavaScript darf nicht lesen, aber das ist ein Cookie-read, kein DOM-access Secure: !devMode, SameSite: http.SameSiteLaxMode, MaxAge: int((8 * 3600)), // 8h — entspricht sessionTTL }) return token } // CSRFTokenFromRequest liest das CSRF-Token aus Form-Feld oder Header. func CSRFTokenFromRequest(r *http.Request) string { // Header hat Vorrang (API-Clients). if h := r.Header.Get(csrfHeaderName); h != "" { return h } // Form-Feld (HTML-Formulare). return r.FormValue(csrfFieldName) } // CSRFProtect ist Middleware für POST/PUT/PATCH/DELETE-Requests. // Sie prüft, ob das CSRF-Token im Request mit dem Cookie übereinstimmt. // GET-/HEAD-/OPTIONS-Requests werden durchgelassen. func CSRFProtect(devMode bool) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodTrace: next.ServeHTTP(w, r) return } cookie, err := r.Cookie(csrfCookieName) if err != nil || cookie.Value == "" { http.Error(w, "CSRF-Token fehlt (Cookie)", http.StatusForbidden) return } token := CSRFTokenFromRequest(r) if token == "" || token != cookie.Value { http.Error(w, "Ungültiger CSRF-Token", http.StatusForbidden) return } next.ServeHTTP(w, r) }) } }