Files
keywarden/internal/security/csrf.go
T
scriptos fd13e67aef
Release Docker Image / Build & Push Docker Image (release) Failing after 1m30s
Release: v0.1.0-alpha
2026-04-05 16:56:16 +02:00

71 lines
2.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Keywarden - Centralized SSH Key Management and Deployment
// Copyright (C) 2026 Patrick Asmus (scriptos)
// SPDX-License-Identifier: AGPL-3.0-or-later
package security
import (
"crypto/rand"
"crypto/subtle"
"fmt"
"net/http"
)
// CSRFMiddleware returns middleware that implements the Double-Submit Cookie
// pattern for CSRF protection.
//
// On every request a _csrf cookie is ensured (generated if absent). On
// state-changing methods (POST, PUT, DELETE, PATCH) the middleware validates
// that the request carries a matching token either as:
// - a form field named "_csrf", or
// - an X-CSRF-Token request header (for AJAX / fetch calls).
//
// The cookie is NOT HttpOnly so that client-side JavaScript can read the
// value and inject it into forms / fetch headers automatically.
func CSRFMiddleware(secureCookies bool) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// --- Ensure CSRF token exists ---
token := ""
if c, err := r.Cookie("_csrf"); err == nil && len(c.Value) == 64 {
token = c.Value
}
if token == "" {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
token = fmt.Sprintf("%x", b)
http.SetCookie(w, &http.Cookie{
Name: "_csrf",
Value: token,
Path: "/",
HttpOnly: false, // JS must be able to read it
Secure: secureCookies,
SameSite: http.SameSiteStrictMode,
MaxAge: 86400, // 24 hours
})
}
// --- Validate on state-changing methods ---
if r.Method == http.MethodPost || r.Method == http.MethodPut ||
r.Method == http.MethodDelete || r.Method == http.MethodPatch {
// Accept the token from either the form body or a request header
submitted := r.FormValue("_csrf")
if submitted == "" {
submitted = r.Header.Get("X-CSRF-Token")
}
if subtle.ConstantTimeCompare([]byte(submitted), []byte(token)) != 1 {
http.Error(w, "Forbidden invalid or missing CSRF token", http.StatusForbidden)
return
}
}
next.ServeHTTP(w, r)
})
}
}