fix: P2+P3 bug fixes, hardening, and cleanup (18 files)

Bug fixes:
- Add applyEnvOverrides to LoadFromBytes (M05)
- Set state=failed on compose-up failure in selfupdate (M16)
- Clamp usableMB to min 0 in memory check (M22)
- Remove "manual" schedule from triggerAllCrossBackups (M23)
- Add mmcblk device handling for partition paths (M21)
- Fix stripPartition for mmcblk devices (L25)
- Fix TruncateStr for UTF-8 and negative maxLen (L05/L06)
- Fix AllDone to return false for empty restore plans (L14)
- Fix PushOnce to return actual errors (L39)
- Restore pending events on save failure in DrainPendingEvents (M03)
- Add duplicate check in AddStoragePath (M04)
- Call CleanupTempMounts after drive scan (H13)
- Log SetStep save errors (M25)

Hardening:
- Guard scheduler Start() against double-start (M14)
- Acquire mutex in scheduler Stop() before reading cancel (L24)
- Cap log lines parameter to 10000 (L31)
- Require POST for logout (L32)
- Use sync.Once for Server.Close() (L49)
- Panic on crypto/rand.Read failure in setup CSRF (L40)
- Validate Bearer token against Hub API key in CSRF (H16 fix)
- Replace custom hasPrefix with strings.HasPrefix (L13)
- Replace simpleHash with crc32.ChecksumIEEE (L48)

Cleanup:
- Remove dead imageName function (L02)
- Remove dead detectHostIPViaRoute function (L03)
- Rename shadowed copy variable to cp (L07)
- Copy DefaultEnabledEvents in GetNotificationPrefs early return (L09)
- Update BUGHUNT.md with comprehensive audit results

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 13:47:52 +01:00
parent 8b8c04a487
commit 45f75a916c
18 changed files with 698 additions and 843 deletions
+1 -2
View File
@@ -13,8 +13,7 @@ const csrfFormField = "_csrf"
func generateCSRFToken() string {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
// Fallback to time-based (extremely unlikely)
return "fallback-csrf-token"
panic("crypto/rand.Read failed: " + err.Error())
}
return hex.EncodeToString(b)
}
+2 -32
View File
@@ -3,7 +3,6 @@ package setup
import (
"net"
"os"
"os/exec"
"strings"
)
@@ -11,46 +10,17 @@ import (
// Inside a Docker container, the network interfaces only show the bridge IP
// (e.g. 172.18.0.4), which is useless for users. Instead, we:
// 1. Check HOST_IP env var (set by docker-compose.yml)
// 2. Try to detect the Docker host gateway via `ip route`
// 3. Fall back to interface enumeration as last resort
// 2. Fall back to interface enumeration as last resort
func DetectLocalIPs() []string {
// Option 1: explicit HOST_IP from environment
if hostIP := os.Getenv("HOST_IP"); hostIP != "" {
return []string{hostIP}
}
// Option 2: detect Docker host gateway IP via default route
// Inside a container, `ip route | grep default` gives the host gateway.
// Then we check the host's IP by looking at what IP routes to that gateway.
if ip := detectHostIPViaRoute(); ip != "" {
return []string{ip}
}
// Option 3: fallback to interface enumeration (works on bare metal)
// Option 2: fallback to interface enumeration (works on bare metal)
return detectInterfaceIPs()
}
// detectHostIPViaRoute tries to find the Docker host's LAN IP.
// Inside a container, the default gateway is the Docker host.
// We read /host-etc/hostname or use the gateway as a hint.
func detectHostIPViaRoute() string {
// Try: ip route get 1.0.0.0 — shows the source IP used for routing
out, err := exec.Command("ip", "route", "get", "1.0.0.0").Output()
if err != nil {
return ""
}
// Output: "1.0.0.0 via 172.18.0.1 dev eth0 src 172.18.0.4"
// The gateway (172.18.0.1) is the Docker host — but that's the bridge IP.
// We need the host's actual LAN IP.
// Better approach: read /proc/net/route or parse `ip route` for the gateway,
// then the gateway itself is the Docker host — but we need its external IP.
// Since we can't easily get the host's LAN IP from inside the container,
// return empty and let the fallback handle it or rely on HOST_IP env.
_ = out
return ""
}
func detectInterfaceIPs() []string {
ifaces, err := net.Interfaces()
if err != nil {
+5
View File
@@ -266,6 +266,11 @@ func countValid(results []DriveBackup) int {
func (s *Server) runDriveScan() {
results, err := ScanDrivesForInfraBackups(s.logger, s.isDebug())
// Clean up any temporary mounts created during scan
if results != nil {
CleanupTempMounts(results, s.logger)
}
s.scanMu.Lock()
defer s.scanMu.Unlock()
+1 -1
View File
@@ -101,7 +101,7 @@ func (s *SetupState) SetStep(step string) {
s.Step = step
s.mu.Unlock()
if err := s.Save(); err != nil {
// Best effort — don't crash
log.Printf("[WARN] Failed to save setup step %q: %v", step, err)
}
}