refactor: replace custom CSRF and rate-limiting with off-the-shelf libraries
All checks were successful
check / check (push) Successful in 4s

Replace custom CSRF middleware with gorilla/csrf and custom rate-limiting
middleware with go-chi/httprate, as requested in code review.

CSRF changes:
- Replace session-based CSRF tokens with gorilla/csrf cookie-based
  double-submit pattern (HMAC-authenticated cookies)
- Keep same form field name (csrf_token) for template compatibility
- Keep same route exclusions (webhook/API routes)
- In dev mode, mark requests as plaintext HTTP to skip Referer check

Rate limiting changes:
- Replace custom token-bucket rate limiter with httprate sliding-window
  counter (per-IP, 5 POST requests/min on login endpoint)
- Remove custom IP extraction (httprate.KeyByRealIP handles
  X-Forwarded-For, X-Real-IP, True-Client-IP)
- Remove custom cleanup goroutine (httprate manages its own state)

Kept as-is:
- SSRF prevention code (internal/delivery/ssrf.go) — application-specific
- CSRFToken() wrapper function — handlers unchanged

Updated README security section and architecture overview to reflect
library choices.
This commit is contained in:
clawbot
2026-03-10 10:05:38 -07:00
parent 5c69efb5bc
commit 0829f9a75d
11 changed files with 126 additions and 317 deletions

View File

@@ -39,6 +39,7 @@ type SessionParams struct {
// Session manages encrypted session storage
type Session struct {
store *sessions.CookieStore
key []byte // raw 32-byte auth key, also used for CSRF cookie signing
log *slog.Logger
config *config.Config
}
@@ -79,6 +80,7 @@ func New(lc fx.Lifecycle, params SessionParams) (*Session, error) {
SameSite: http.SameSiteLaxMode,
}
s.key = keyBytes
s.store = store
s.log.Info("session manager initialized")
return nil
@@ -93,6 +95,12 @@ func (s *Session) Get(r *http.Request) (*sessions.Session, error) {
return s.store.Get(r, SessionName)
}
// GetKey returns the raw 32-byte authentication key used for session
// encryption. This key is also suitable for CSRF cookie signing.
func (s *Session) GetKey() []byte {
return s.key
}
// Save saves the session
func (s *Session) Save(r *http.Request, w http.ResponseWriter, sess *sessions.Session) error {
return sess.Save(r, w)

View File

@@ -34,7 +34,7 @@ func testSession(t *testing.T) *Session {
}
log := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))
return NewForTest(store, cfg, log)
return NewForTest(store, cfg, log, key)
}
// --- Get and Save Tests ---

View File

@@ -9,10 +9,13 @@ import (
// NewForTest creates a Session with a pre-configured cookie store for use
// in tests. This bypasses the fx lifecycle and database dependency, allowing
// middleware and handler tests to use real session functionality.
func NewForTest(store *sessions.CookieStore, cfg *config.Config, log *slog.Logger) *Session {
// middleware and handler tests to use real session functionality. The key
// parameter is the raw 32-byte authentication key used for session encryption
// and CSRF cookie signing.
func NewForTest(store *sessions.CookieStore, cfg *config.Config, log *slog.Logger, key []byte) *Session {
return &Session{
store: store,
key: key,
config: cfg,
log: log,
}