v0.76.0: campaign-#3 hardening (settings .bak recovery, restore stack_name validation, quiesce marker quarantine)

S1: corrupt settings.json recovers from .bak / safe-defaults+preserve, no crash-loop.
F2: validStackName gates restore + export handlers (reject /,\,..,NUL traversal).
S3: corrupt quiesce marker logged + quarantined, not silently dropped.
Tests T-S1/F2/S3 + red-proofs. Agent/hub untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 23:34:33 +02:00
parent 141d51bf19
commit b0dd13154b
10 changed files with 305 additions and 2 deletions
+8
View File
@@ -151,6 +151,7 @@ func (s *Server) dashboardHandler(w http.ResponseWriter, r *http.Request) {
sysInfo := system.GetInfo(s.primaryHDDPath(), s.cpuCollector)
data := s.baseData("dashboard", "Vezérlőpult")
data["SettingsWarning"] = s.settings.LoadWarning // non-empty if settings.json was recovered from corruption
data["Stacks"] = deployedStacks
data["MissingStorage"] = s.missingStorageMap(deployedStacks)
data["RunningCount"] = running
@@ -795,6 +796,13 @@ func (s *Server) backupRestoreHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/backups?flash_error=Hi%C3%A1nyz%C3%B3+param%C3%A9terek", http.StatusFound)
return
}
// F2 (defense-in-depth): a stack name is a single segment, never a path. Reject traversal before any
// restore work — never let it reach RestoreFromRecoveryUnit.
if !validStackName(stackName) {
s.logger.Printf("[WARN] [web] restore rejected: invalid stack_name %q from %s", stackName, r.RemoteAddr)
http.Redirect(w, r, "/backups?flash_error=%C3%89rv%C3%A9nytelen+alkalmaz%C3%A1sn%C3%A9v", http.StatusFound)
return
}
if s.backupMgr == nil {
http.Redirect(w, r, "/backups?flash_error=Ment%C3%A9s+nincs+be%C3%A1ll%C3%ADtva", http.StatusFound)