v0.23.0 — CSRF protection on all browser-facing POST endpoints
Controller: - internal/web/csrf.go (new): CsrfProtect middleware, csrfToken/csrfField helpers - auth.go: per-session CSRF token (csrfToken field, csrfTokenForSession method) - server.go: executeTemplate wrapper auto-injects CSRFField+CSRFToken - main.go: wire CsrfProtect on all routes; bump to v0.23.0 - handlers.go, storage_handlers.go, handler_restore.go: executeTemplate - All templates: CSRFField in forms, meta csrf-token, csrfHeaders() JS helper, fetch calls updated; sendBeacon→fetch+keepalive in storage_attach.html Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,44 @@
|
||||
## Changelog
|
||||
|
||||
### v0.23.0 — CSRF Protection (2026-02-21)
|
||||
|
||||
**CSRF (Cross-Site Request Forgery) protection on all browser-facing POST endpoints — controller and hub.**
|
||||
|
||||
**Controller changes:**
|
||||
|
||||
- New `internal/web/csrf.go`: `CsrfProtect` HTTP middleware validates CSRF tokens on all state-mutating requests (POST/DELETE/PATCH).
|
||||
- Reads token from `_csrf` form field or `X-CSRF-Token` request header.
|
||||
- Exempt paths: `Authorization: Bearer` requests (selfupdate, config/apply hub→controller calls) — browsers cannot auto-send Bearer headers, so no CSRF risk.
|
||||
- Auth-disabled mode (no password set): CSRF check is skipped entirely.
|
||||
- On rejection: JSON error for `/api/` paths, HTTP 403 text for page routes.
|
||||
- `internal/web/auth.go`: `session` struct gains a `csrfToken string` field. `createSession()` generates a second 32-byte random CSRF token alongside the session token. New `csrfTokenForSession(sessionToken)` method returns the CSRF token for a given session.
|
||||
- `internal/web/server.go`: New `executeTemplate(w, r, name, data)` wrapper auto-injects `CSRFField` (`template.HTML` hidden input) and `CSRFToken` (raw string) into every page render data map.
|
||||
- `cmd/controller/main.go`: All route registrations wrapped with `webServer.CsrfProtect(...)` middleware. Version bumped to `v0.23.0`.
|
||||
- All handlers (`handlers.go`, `storage_handlers.go`, `handler_restore.go`): Switched from `s.render(w, ...)` to `s.executeTemplate(w, r, ...)`.
|
||||
- All templates updated:
|
||||
- `layout.html`: Added `<meta name="csrf-token">` and inline `csrfHeaders()` JS helper (returns `{'X-CSRF-Token': ...}`) in `<head>` (before page-specific scripts). Updated 4 fetch POST/DELETE calls.
|
||||
- `settings.html`: Added `{{$.CSRFField}}` to 5 forms inside `{{range .StoragePaths}}` (must use `$` for outer scope inside range). Added `{{.CSRFField}}` to 3 page-level forms. Inline-label form uses `document.querySelector('meta[name="csrf-token"]').content`. Updated 5 fetch calls.
|
||||
- `deploy.html`: Added `{{.CSRFField}}` to cross-backup form. Updated 3 fetch calls.
|
||||
- `backups.html`: Updated 3 fetch calls. Dynamically-created restore form injects `_csrf` from meta tag.
|
||||
- `storage_init.html`, `storage_attach.html`, `migrate.html`, `migrate_drive.html`, `app_info.html`, `restore.html`: All fetch calls updated.
|
||||
- `storage_attach.html`: Replaced `navigator.sendBeacon()` with `fetch(..., {keepalive: true})` — `sendBeacon` cannot send custom headers, making CSRF impossible.
|
||||
|
||||
**Hub changes (v0.3.8):**
|
||||
|
||||
- `internal/web/server.go`: Replaced insecure literal `hub_session=authenticated` cookie with proper server-side session map.
|
||||
- New `hubSession` struct with `csrfToken string` and `expiresAt time.Time`.
|
||||
- `sessions map[string]*hubSession` + `sessionsMu sync.RWMutex` on `Server` struct.
|
||||
- `handleLogin`: Generates cryptographically random 64-char hex session token + 64-char hex CSRF token. Cookie gains `SameSite=Lax` and `Secure` (when TLS) attributes. Session expires after 7 days.
|
||||
- `RequireAuth`: Validates session token against map (constant-time compare), redirects to `/login` on failure.
|
||||
- `CleanupSessions(ctx)`: Goroutine that purges expired sessions every hour.
|
||||
- CSRF validation block at top of `ServeHTTP`: checks `X-CSRF-Token` header or `_csrf` form field on POST/DELETE/PATCH. Skips when no session cookie (Basic Auth / API path).
|
||||
- `csrfToken(r)`, `csrfField(r)` helpers for template data injection.
|
||||
- `internal/web/configs.go`: Added `html/template` import. All template render calls pass `CSRFField template.HTML` and/or `CSRFToken string`. `renderConfigForm` gains `r *http.Request` parameter.
|
||||
- Templates updated:
|
||||
- `config_form.html`: Added `{{.CSRFField}}` inside the `<form>`.
|
||||
- `customer_unified.html`: Added `<meta name="csrf-token">` + inline `csrfHeaders()` in `<head>`. Added `{{.CSRFField}}` to all 5 POST forms (unblock, block, delete config, create-config, regen-password). Updated 3 JS fetch POST calls (trigger-update, push-config, pull-config).
|
||||
- `cmd/hub/main.go`: Started `go webServer.CleanupSessions(ctx)` goroutine.
|
||||
|
||||
### v0.22.3 — Hub Asset Sync (2026-02-21)
|
||||
|
||||
**Hub-managed asset downloads**
|
||||
|
||||
Reference in New Issue
Block a user