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
+29 -29
View File
@@ -174,7 +174,7 @@ func (s *Server) dashboardHandler(w http.ResponseWriter, _ *http.Request) {
data["DiskWarnings"] = s.alertManager.GetInlineAlerts("dashboard")
}
s.render(w, "dashboard", data)
s.executeTemplate(w, r, "dashboard", data)
}
func (s *Server) stacksHandler(w http.ResponseWriter, _ *http.Request) {
@@ -201,7 +201,7 @@ func (s *Server) stacksHandler(w http.ResponseWriter, _ *http.Request) {
}
data["StorageLabels"] = storageLabels
s.render(w, "stacks", data)
s.executeTemplate(w, r, "stacks", data)
}
func (s *Server) logsHandler(w http.ResponseWriter, r *http.Request, name string) {
@@ -226,7 +226,7 @@ func (s *Server) logsHandler(w http.ResponseWriter, r *http.Request, name string
data := s.baseData("logs", stack.Meta.DisplayName+" — Naplók")
data["Stack"] = stack
data["Logs"] = logs
s.render(w, "logs", data)
s.executeTemplate(w, r, "logs", data)
}
func (s *Server) deployHandler(w http.ResponseWriter, r *http.Request, name string) {
@@ -370,7 +370,7 @@ func (s *Server) deployHandler(w http.ResponseWriter, r *http.Request, name stri
data["FlashError"] = flashErr
}
s.render(w, "deploy", data)
s.executeTemplate(w, r, "deploy", data)
}
func (s *Server) appDetailHandler(w http.ResponseWriter, r *http.Request, slug string) {
@@ -403,7 +403,7 @@ func (s *Server) appDetailHandler(w http.ResponseWriter, r *http.Request, slug s
data["HasAppInfo"] = found.Meta.HasAppInfo()
data["HasOptionalConfig"] = found.Meta.HasOptionalConfig()
s.render(w, "app_info", data)
s.executeTemplate(w, r, "app_info", data)
}
func (s *Server) monitoringHandler(w http.ResponseWriter, _ *http.Request) {
@@ -453,7 +453,7 @@ func (s *Server) monitoringHandler(w http.ResponseWriter, _ *http.Request) {
data["AllPingsConfigured"] = allConfigured
}
s.render(w, "monitoring", data)
s.executeTemplate(w, r, "monitoring", data)
}
// isPingConfigured returns true if a healthcheck ping UUID is non-empty and not a placeholder.
@@ -638,7 +638,7 @@ func (s *Server) backupsHandler(w http.ResponseWriter, r *http.Request) {
data["Backup"] = nil
}
s.render(w, "backups", data)
s.executeTemplate(w, r, "backups", data)
}
// Tier2DriveGroup holds grouped Tier 2 cross-drive backup items for one destination drive.
@@ -1017,7 +1017,7 @@ func (s *Server) settingsHandler(w http.ResponseWriter, r *http.Request) {
if msg := r.URL.Query().Get("storage_msg"); msg == "success" {
data["StorageSuccess"] = r.URL.Query().Get("storage_detail")
}
s.render(w, "settings", data)
s.executeTemplate(w, r, "settings", data)
}
func (s *Server) settingsPasswordHandler(w http.ResponseWriter, r *http.Request) {
@@ -1032,21 +1032,21 @@ func (s *Server) settingsPasswordHandler(w http.ResponseWriter, r *http.Request)
effectiveHash := s.effectivePasswordHash()
if err := bcrypt.CompareHashAndPassword([]byte(effectiveHash), []byte(currentPassword)); err != nil {
data["PasswordError"] = "Hibás jelenlegi jelszó"
s.render(w, "settings", data)
s.executeTemplate(w, r, "settings", data)
return
}
// Validate new password length
if len(newPassword) < 8 {
data["PasswordError"] = "A jelszónak legalább 8 karakter hosszúnak kell lennie"
s.render(w, "settings", data)
s.executeTemplate(w, r, "settings", data)
return
}
// Validate passwords match
if newPassword != confirmPassword {
data["PasswordError"] = "A két jelszó nem egyezik"
s.render(w, "settings", data)
s.executeTemplate(w, r, "settings", data)
return
}
@@ -1055,7 +1055,7 @@ func (s *Server) settingsPasswordHandler(w http.ResponseWriter, r *http.Request)
if err != nil {
s.logger.Printf("[ERROR] Failed to hash new password: %v", err)
data["PasswordError"] = "Belső hiba a jelszó mentésekor"
s.render(w, "settings", data)
s.executeTemplate(w, r, "settings", data)
return
}
@@ -1063,7 +1063,7 @@ func (s *Server) settingsPasswordHandler(w http.ResponseWriter, r *http.Request)
if err := s.settings.SetPasswordHash(string(hash)); err != nil {
s.logger.Printf("[ERROR] Failed to save password to settings.json: %v", err)
data["PasswordError"] = "Belső hiba a jelszó mentésekor"
s.render(w, "settings", data)
s.executeTemplate(w, r, "settings", data)
return
}
@@ -1126,7 +1126,7 @@ func (s *Server) settingsNotificationsHandler(w http.ResponseWriter, r *http.Req
s.logger.Printf("[ERROR] Failed to save notification prefs: %v", err)
data := s.settingsData()
data["NotificationError"] = "Hiba a beállítások mentésekor"
s.render(w, "settings", data)
s.executeTemplate(w, r, "settings", data)
return
}
@@ -1144,7 +1144,7 @@ func (s *Server) settingsNotificationsHandler(w http.ResponseWriter, r *http.Req
} else {
data["NotificationSuccess"] = "Értesítési beállítások mentve."
}
s.render(w, "settings", data)
s.executeTemplate(w, r, "settings", data)
}
func (s *Server) settingsNotificationsTestHandler(w http.ResponseWriter, r *http.Request) {
@@ -1152,7 +1152,7 @@ func (s *Server) settingsNotificationsTestHandler(w http.ResponseWriter, r *http
if s.notifier == nil {
data["NotificationError"] = "Az értesítések nincsenek bekapcsolva"
s.render(w, "settings", data)
s.executeTemplate(w, r, "settings", data)
return
}
@@ -1160,12 +1160,12 @@ func (s *Server) settingsNotificationsTestHandler(w http.ResponseWriter, r *http
if err != nil {
s.logger.Printf("[ERROR] Test notification failed: %v", err)
data["NotificationError"] = fmt.Sprintf("Teszt email küldése sikertelen: %v", err)
s.render(w, "settings", data)
s.executeTemplate(w, r, "settings", data)
return
}
data["NotificationSuccess"] = "Teszt email elküldve."
s.render(w, "settings", data)
s.executeTemplate(w, r, "settings", data)
}
// --- Storage path management handlers ---
@@ -1279,21 +1279,21 @@ func (s *Server) settingsStorageAddHandler(w http.ResponseWriter, r *http.Reques
fi, err := os.Stat(path)
if err != nil || !fi.IsDir() {
data["StorageError"] = "Az útvonal nem létezik vagy nem mappa."
s.render(w, "settings", data)
s.executeTemplate(w, r, "settings", data)
return
}
// 2. Is mount point
if !system.IsMountPoint(path) {
data["StorageError"] = "Ez az útvonal nem külön csatlakoztatott meghajtó. Adatok az SSD-re kerülnének!"
s.render(w, "settings", data)
s.executeTemplate(w, r, "settings", data)
return
}
// 3. Writable
if !system.IsWritable(path) {
data["StorageError"] = "Az útvonal nem írható."
s.render(w, "settings", data)
s.executeTemplate(w, r, "settings", data)
return
}
@@ -1301,7 +1301,7 @@ func (s *Server) settingsStorageAddHandler(w http.ResponseWriter, r *http.Reques
for _, existing := range s.settings.GetStoragePaths() {
if system.PathsOverlap(path, existing.Path) {
data["StorageError"] = fmt.Sprintf("Az útvonal átfedi a már regisztrált %s útvonalat.", existing.Path)
s.render(w, "settings", data)
s.executeTemplate(w, r, "settings", data)
return
}
}
@@ -1322,7 +1322,7 @@ func (s *Server) settingsStorageAddHandler(w http.ResponseWriter, r *http.Reques
if err := s.settings.AddStoragePath(sp); err != nil {
s.logger.Printf("[ERROR] Failed to add storage path: %v", err)
data["StorageError"] = "Hiba a mentés során."
s.render(w, "settings", data)
s.executeTemplate(w, r, "settings", data)
return
}
@@ -1341,7 +1341,7 @@ func (s *Server) settingsStorageRemoveHandler(w http.ResponseWriter, r *http.Req
apps := s.appsUsingPath(path)
if len(apps) > 0 {
data["StorageError"] = fmt.Sprintf("Nem törölhető: az alábbi alkalmazások használják: %s", strings.Join(apps, ", "))
s.render(w, "settings", data)
s.executeTemplate(w, r, "settings", data)
return
}
@@ -1349,7 +1349,7 @@ func (s *Server) settingsStorageRemoveHandler(w http.ResponseWriter, r *http.Req
for _, sp := range s.settings.GetStoragePaths() {
if sp.Path == path && sp.IsDefault {
data["StorageError"] = "Az alapértelmezett adattároló nem törölhető."
s.render(w, "settings", data)
s.executeTemplate(w, r, "settings", data)
return
}
}
@@ -1357,13 +1357,13 @@ func (s *Server) settingsStorageRemoveHandler(w http.ResponseWriter, r *http.Req
// Check: last path
if len(s.settings.GetStoragePaths()) <= 1 {
data["StorageError"] = "Az utolsó adattároló nem törölhető."
s.render(w, "settings", data)
s.executeTemplate(w, r, "settings", data)
return
}
if err := s.settings.RemoveStoragePath(path); err != nil {
data["StorageError"] = "Hiba a törlés során."
s.render(w, "settings", data)
s.executeTemplate(w, r, "settings", data)
return
}
@@ -1406,7 +1406,7 @@ func (s *Server) settingsStorageLabelHandler(w http.ResponseWriter, r *http.Requ
if label == "" || len(label) > 50 {
data := s.settingsData()
data["StorageError"] = "A megnevezés nem lehet üres és legfeljebb 50 karakter."
s.render(w, "settings", data)
s.executeTemplate(w, r, "settings", data)
return
}
@@ -1414,7 +1414,7 @@ func (s *Server) settingsStorageLabelHandler(w http.ResponseWriter, r *http.Requ
s.logger.Printf("[ERROR] Failed to set storage label: %v", err)
data := s.settingsData()
data["StorageError"] = "Hiba a megnevezés mentésekor."
s.render(w, "settings", data)
s.executeTemplate(w, r, "settings", data)
return
}