hub v0.3.8 — CSRF protection + secure session model

- server.go: replace literal hub_session=authenticated with random 64-char hex
  session tokens stored server-side (hubSession map + sync.RWMutex); per-session
  CSRF tokens; CleanupSessions goroutine; SameSite=Lax+Secure cookie; CSRF
  validation in ServeHTTP; csrfToken/csrfField helpers
- configs.go: add html/template import; pass CSRFField/CSRFToken to all template
  renders; renderConfigForm gains r *http.Request parameter
- config_form.html: {{.CSRFField}} in form
- customer_unified.html: meta csrf-token + csrfHeaders() JS; {{.CSRFField}} in
  all 5 POST forms; csrfHeaders() on 3 fetch calls
- main.go: start CleanupSessions goroutine

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 16:39:14 +01:00
parent da991fad57
commit 67f53a4ccd
6 changed files with 187 additions and 21 deletions
+16 -3
View File
@@ -4,6 +4,7 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"html/template"
"io"
"net/http"
"regexp"
@@ -106,10 +107,12 @@ func (s *Server) handleConfigList(w http.ResponseWriter, r *http.Request) {
Customers []customerListEntry
ActiveNav string
Flash string
CSRFToken string
}{
Customers: entries,
ActiveNav: "configs",
Flash: r.URL.Query().Get("flash"),
CSRFToken: s.csrfToken(r),
}
s.templates.ExecuteTemplate(w, "configs.html", data)
}
@@ -276,6 +279,8 @@ func (s *Server) handleCustomerUnified(w http.ResponseWriter, r *http.Request, c
Flash string
ActiveNav string
CSRFField template.HTML
CSRFToken string
}
data := pageData{
@@ -312,6 +317,8 @@ func (s *Server) handleCustomerUnified(w http.ResponseWriter, r *http.Request, c
Flash: r.URL.Query().Get("flash"),
ActiveNav: "configs",
CSRFField: s.csrfField(r),
CSRFToken: s.csrfToken(r),
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
@@ -328,11 +335,13 @@ func (s *Server) handleConfigNewForm(w http.ResponseWriter, r *http.Request) {
Overrides map[string]interface{}
ActiveNav string
Error string
CSRFField template.HTML
}{
IsNew: true,
Config: &store.CustomerConfig{},
Overrides: make(map[string]interface{}),
ActiveNav: "configs",
CSRFField: s.csrfField(r),
}
s.templates.ExecuteTemplate(w, "config_form.html", data)
}
@@ -346,7 +355,7 @@ func (s *Server) handleConfigCreate(w http.ResponseWriter, r *http.Request) {
customerID := strings.TrimSpace(r.FormValue("customer_id"))
if customerID == "" || !validCustomerID.MatchString(customerID) {
s.renderConfigForm(w, true, &store.CustomerConfig{
s.renderConfigForm(w, r, true, &store.CustomerConfig{
CustomerName: r.FormValue("customer_name"),
Domain: r.FormValue("domain"),
Email: r.FormValue("email"),
@@ -357,7 +366,7 @@ func (s *Server) handleConfigCreate(w http.ResponseWriter, r *http.Request) {
// Check for duplicates
existing, _ := s.store.GetCustomerConfig(customerID)
if existing != nil {
s.renderConfigForm(w, true, &store.CustomerConfig{
s.renderConfigForm(w, r, true, &store.CustomerConfig{
CustomerID: customerID,
CustomerName: r.FormValue("customer_name"),
Domain: r.FormValue("domain"),
@@ -418,11 +427,13 @@ func (s *Server) handleConfigEditForm(w http.ResponseWriter, r *http.Request, cu
Overrides map[string]interface{}
ActiveNav string
Error string
CSRFField template.HTML
}{
IsNew: false,
Config: cfg,
Overrides: overrides,
ActiveNav: "configs",
CSRFField: s.csrfField(r),
}
s.templates.ExecuteTemplate(w, "config_form.html", data)
}
@@ -655,7 +666,7 @@ func (s *Server) handleCreateConfigFromReport(w http.ResponseWriter, r *http.Req
}
// renderConfigForm is a helper to re-render the form with an error.
func (s *Server) renderConfigForm(w http.ResponseWriter, isNew bool, cfg *store.CustomerConfig, overrides map[string]interface{}, errMsg string) {
func (s *Server) renderConfigForm(w http.ResponseWriter, r *http.Request, isNew bool, cfg *store.CustomerConfig, overrides map[string]interface{}, errMsg string) {
if overrides == nil {
overrides = make(map[string]interface{})
}
@@ -665,12 +676,14 @@ func (s *Server) renderConfigForm(w http.ResponseWriter, isNew bool, cfg *store.
Overrides map[string]interface{}
ActiveNav string
Error string
CSRFField template.HTML
}{
IsNew: isNew,
Config: cfg,
Overrides: overrides,
ActiveNav: "configs",
Error: errMsg,
CSRFField: s.csrfField(r),
}
s.templates.ExecuteTemplate(w, "config_form.html", data)
}