fix: security hardening for middleware, crypto, and backup handlers

- Remove CORS origin reflection (SEC-C1 CRITICAL)
- Add Content-Security-Policy header (SEC-H2)
- Fix rate limiter memory leak with periodic stale IP cleanup (SEC-H5)
- Enforce minimum 32-char ENCRYPTION_KEY (SEC-H4)
- Validate backup type against allowlist (SEC-M6)
- Fix backup download path traversal with path containment check (SEC-C2 CRITICAL)
This commit is contained in:
2026-04-04 12:40:37 +03:00
parent c6693a2ef5
commit ff59d9f799
4 changed files with 69 additions and 17 deletions
+24 -2
View File
@@ -6,6 +6,7 @@ import (
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/alexei/docker-watcher/internal/store"
@@ -69,14 +70,35 @@ func (s *Server) downloadBackup(w http.ResponseWriter, r *http.Request) {
}
filePath := s.backupEngine.FilePath(backup)
if _, err := os.Stat(filePath); err != nil {
// Validate the resolved path stays within the backup directory to prevent path traversal.
absPath, err := filepath.Abs(filePath)
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to resolve backup path")
return
}
absBackupDir, _ := filepath.Abs(s.backupEngine.BackupDir())
if !strings.HasPrefix(absPath, absBackupDir+string(filepath.Separator)) {
respondError(w, http.StatusForbidden, "access denied")
return
}
f, err := os.Open(absPath)
if err != nil {
respondError(w, http.StatusNotFound, "backup file not found on disk")
return
}
defer f.Close()
stat, err := f.Stat()
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to read backup file")
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", "attachment; filename=\""+filepath.Base(backup.Filename)+"\"")
http.ServeFile(w, r, filePath)
http.ServeContent(w, r, filepath.Base(backup.Filename), stat.ModTime(), f)
}
// deleteBackup handles DELETE /api/backups/{id}.