From fcb806c3445ce56f2e57e208de773b4dac957c8d Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Tue, 15 Sep 2026 20:35:23 -0400 Subject: fix: owner re-entrancy deadlock, telnet write queue, recovery/refund money safety, admin path traversal --- internal/admin/api_files.go | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) (limited to 'internal/admin/api_files.go') diff --git a/internal/admin/api_files.go b/internal/admin/api_files.go index 496b08d..f2ef928 100644 --- a/internal/admin/api_files.go +++ b/internal/admin/api_files.go @@ -1,6 +1,7 @@ package admin import ( + "fmt" "io" "net/http" "os" @@ -8,6 +9,17 @@ import ( "strings" ) +// resolveDataPath resolves a request-supplied path inside the admin data +// directory, rejecting paths that would escape it via .. segments. The +// returned path is always filepath.Join(dataDir, cleaned relative path). +func resolveDataPath(dataDir, relPath string) (string, error) { + clean := filepath.Clean(strings.TrimPrefix(strings.TrimPrefix(relPath, "/"), "data/")) + if clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) { + return "", fmt.Errorf("path escapes the data directory: %s", relPath) + } + return filepath.Join(dataDir, clean), nil +} + func (s *AdminServer) handleFileRename(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) @@ -36,7 +48,11 @@ func (s *AdminServer) handleFileRename(w http.ResponseWriter, r *http.Request) { relPath := strings.TrimPrefix(body.Path, "/") relPath = strings.TrimPrefix(relPath, "data/") - oldAbs := filepath.Join(s.dataDir, relPath) + oldAbs, err := resolveDataPath(s.dataDir, relPath) + if err != nil { + writeJSON(w, map[string]any{"error": err.Error()}) + return + } oldContent, err := snapshotFile(oldAbs) if err != nil { @@ -77,11 +93,16 @@ func (s *AdminServer) handleFiles(w http.ResponseWriter, r *http.Request) { relPath := r.URL.Query().Get("path") relPath = strings.TrimPrefix(relPath, "/") relPath = strings.TrimPrefix(relPath, "data/") + fullPath, pathErr := resolveDataPath(s.dataDir, relPath) + if pathErr != nil { + writeJSON(w, map[string]any{"error": pathErr.Error()}) + return + } switch r.Method { case http.MethodGet: if relPath == "" { - entries, err := os.ReadDir(s.dataDir) + entries, err := os.ReadDir(fullPath) if err != nil { writeJSON(w, map[string]any{"error": err.Error()}) return @@ -137,8 +158,6 @@ func (s *AdminServer) handleFiles(w http.ResponseWriter, r *http.Request) { writeJSON(w, map[string]any{"error": "missing path"}) return } - fullPath := filepath.Join(s.dataDir, relPath) - body, err := io.ReadAll(r.Body) if err != nil { writeJSON(w, map[string]any{"error": "read body: " + err.Error()}) @@ -180,7 +199,6 @@ func (s *AdminServer) handleFiles(w http.ResponseWriter, r *http.Request) { writeJSON(w, map[string]any{"error": "missing path"}) return } - fullPath := filepath.Join(s.dataDir, relPath) oldContent, _ := snapshotFile(fullPath) if err := os.Remove(fullPath); err != nil { writeJSON(w, map[string]any{"error": "delete error: " + err.Error()}) -- cgit v1.2.3