update_server/internal/http/csrf.go
2026-06-10 20:51:17 +03:00

156 lines
3.6 KiB
Go

package httpserver
import (
"context"
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"fmt"
"net/http"
"strings"
"time"
)
const (
adminCookiePath = "/admin"
csrfFormField = "csrf_token"
csrfHeaderName = "X-CSRF-Token"
)
type csrfTokenContextKey struct{}
func (h *handler) adminCSRF(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token, cookie, err := h.ensureCSRFCookie(r)
if err != nil {
http.Error(w, "csrf setup failed", http.StatusInternalServerError)
return
}
if cookie != nil {
http.SetCookie(w, cookie)
}
r = r.WithContext(context.WithValue(r.Context(), csrfTokenContextKey{}, token))
if requiresCSRFProtection(r.Method) {
submittedToken, err := h.submittedCSRFToken(w, r)
if err != nil || subtle.ConstantTimeCompare([]byte(token), []byte(submittedToken)) != 1 {
http.Error(w, "csrf validation failed", http.StatusForbidden)
return
}
}
next.ServeHTTP(w, r)
})
}
func (h *handler) csrfToken(r *http.Request) string {
if token, ok := r.Context().Value(csrfTokenContextKey{}).(string); ok {
return token
}
cookie, err := r.Cookie(h.config.CSRFCookieName)
if err != nil {
return ""
}
if !validCSRFCookieToken(cookie.Value) {
return ""
}
return cookie.Value
}
func (h *handler) issueCSRFCookie(w http.ResponseWriter) (string, error) {
token, err := generateCSRFToken()
if err != nil {
return "", err
}
http.SetCookie(w, h.csrfCookie(token))
return token, nil
}
func (h *handler) clearCSRFCookie() *http.Cookie {
return &http.Cookie{
Name: h.config.CSRFCookieName,
Value: "",
Path: adminCookiePath,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
Secure: h.config.SecureCookies,
Expires: time.Unix(0, 0).UTC(),
MaxAge: -1,
}
}
func (h *handler) ensureCSRFCookie(r *http.Request) (string, *http.Cookie, error) {
cookie, err := r.Cookie(h.config.CSRFCookieName)
if err == nil && validCSRFCookieToken(cookie.Value) {
return cookie.Value, nil, nil
}
token, err := generateCSRFToken()
if err != nil {
return "", nil, fmt.Errorf("generate csrf token: %w", err)
}
return token, h.csrfCookie(token), nil
}
func (h *handler) csrfCookie(token string) *http.Cookie {
return &http.Cookie{
Name: h.config.CSRFCookieName,
Value: token,
Path: adminCookiePath,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
Secure: h.config.SecureCookies,
}
}
func (h *handler) submittedCSRFToken(w http.ResponseWriter, r *http.Request) (string, error) {
if token := strings.TrimSpace(r.Header.Get(csrfHeaderName)); token != "" {
return token, nil
}
contentType := strings.ToLower(strings.TrimSpace(r.Header.Get("Content-Type")))
if strings.HasPrefix(contentType, "multipart/form-data") {
r.Body = http.MaxBytesReader(w, r.Body, maxUploadRequestLimit(h.config.MaxUploadBytes))
if err := r.ParseMultipartForm(16 << 20); err != nil {
return "", err
}
return strings.TrimSpace(r.FormValue(csrfFormField)), nil
}
if err := r.ParseForm(); err != nil {
return "", err
}
return strings.TrimSpace(r.FormValue(csrfFormField)), nil
}
func generateCSRFToken() (string, error) {
buf := make([]byte, 32)
if _, err := rand.Read(buf); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(buf), nil
}
func requiresCSRFProtection(method string) bool {
switch method {
case http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodTrace:
return false
default:
return true
}
}
func validCSRFCookieToken(token string) bool {
token = strings.TrimSpace(token)
return len(token) >= 32
}