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:
2026-02-21 16:38:56 +01:00
parent ade01470d0
commit 02650e3202
20 changed files with 1143 additions and 75 deletions
+28 -3
View File
@@ -757,6 +757,30 @@ self_update:
- Session cleanup every 15 minutes
- All sessions invalidated on password change
- Conditional logout link (hidden when auth is disabled)
- Each session stores a dedicated CSRF token (separate 32-byte random value) alongside the session token
#### CSRF Protection (`internal/web/csrf.go`)
Synchronizer-token CSRF protection on all browser-facing state-mutating endpoints.
**How it works:**
- `CsrfProtect` middleware wraps all route handlers in `main.go`
- Safe methods (GET, HEAD, OPTIONS) pass through without validation
- For POST/DELETE/PATCH: reads token from `_csrf` form field or `X-CSRF-Token` request header; constant-time compares against the session's stored CSRF token
- On rejection: JSON `{"ok":false,"error":"CSRF token missing or invalid"}` for `/api/` paths; HTTP 403 text page for UI routes
- Logs: `[WARN] CSRF rejected: METHOD /path from addr (reason)`
**Exempt paths (no CSRF check):**
- Requests with `Authorization: Bearer ...` header — hub→controller API calls (selfupdate, config/apply). Browsers cannot auto-send Bearer headers, so cross-site requests are impossible on these endpoints.
- Auth-disabled mode (`authEnabled() == false`) — CSRF is meaningless when there is no session.
**Token delivery to templates:**
- `executeTemplate(w, r, name, data)` wrapper in `server.go` auto-injects `CSRFField` (`template.HTML` hidden `<input>`) and `CSRFToken` (raw string) into every page's data map
- `layout.html` emits `<meta name="csrf-token" content="{{.CSRFToken}}">` and defines `csrfHeaders()` JS function in `<head>` (before page scripts)
- Forms: `{{.CSRFField}}` (or `{{$.CSRFField}}` inside `{{range}}` loops — outer scope required)
- JS `fetch()` calls: `headers: csrfHeaders()` — returns `{'X-CSRF-Token': metaContent}`
- Dynamically-created JS forms: read token from `document.querySelector('meta[name="csrf-token"]').content`
- `navigator.sendBeacon()` replaced with `fetch(..., {keepalive: true})` where used — `sendBeacon` cannot send custom headers
#### Settings Persistence (`internal/settings/settings.go`)
@@ -1033,8 +1057,9 @@ controller/
│ │ └── templates/ # 7 wizard HTML templates (Hungarian)
│ ├── recovery/info.go # Recovery info file generator (recovery-info.txt)
│ └── web/
│ ├── server.go # HTTP server, routing, static files
│ ├── auth.go # Session auth, login/logout, session cleanup
│ ├── server.go # HTTP server, routing, static files, executeTemplate wrapper
│ ├── auth.go # Session auth + per-session CSRF token, login/logout, session cleanup
│ ├── csrf.go # CsrfProtect middleware, csrfToken/csrfField helpers
│ ├── handlers.go # Page handlers (dashboard, stacks, deploy, backups, etc.)
│ ├── handler_restore.go # DR: restore page handler + APIs (scan, restore all, skip)
│ ├── storage_handlers.go # Storage API handlers (scan, format, attach, migrate, cleanup, disconnect/reconnect)
@@ -1329,7 +1354,7 @@ See `docker-compose.yml` for the full volume configuration.
- [ ] Update classification and auto-apply (optional/required/security markers)
- [ ] Docker volume backup (`/var/lib/docker/volumes:ro`)
- [ ] Raspberry Pi testing (pi-customer-1)
- [ ] CSRF protection on POST endpoints
- [x] CSRF protection on POST endpoints (v0.23.0)
- [ ] Login rate limiting
---