package client import ( "encoding/json" "fmt" "os" "path/filepath" "runtime" "strings" "crypto-miner-agent/deploy" ) const ( maxListDirEntries = 500 maxReadFileBytes = 512 * 1024 ) type dirEntry struct { Name string `json:"name"` IsDir bool `json:"is_dir"` Size int64 `json:"size"` } type listDirResponse struct { Path string `json:"path"` HomeDir string `json:"home_dir"` Platform string `json:"platform"` Entries []dirEntry `json:"entries"` } var hiddenDirNames = map[string]bool{ "System Volume Information": true, "$Recycle.Bin": true, "$RECYCLE.BIN": true, } func containsPathTraversal(raw string) bool { raw = strings.TrimSpace(raw) if raw == "" { return false } // Allow ~/ prefix — resolved via home, not traversal. if strings.HasPrefix(raw, "~/") { raw = raw[2:] } raw = strings.ReplaceAll(raw, "\\", "/") for _, part := range strings.Split(raw, "/") { if part == ".." { return true } } return false } func userHomeDir() (string, error) { home, err := os.UserHomeDir() if err != nil || home == "" { return "", fmt.Errorf("home directory unavailable") } return filepath.Clean(home), nil } func resolveListDirPath(path string) (string, error) { path = strings.TrimSpace(path) if path == "" || path == "~" || path == "@home" { return userHomeDir() } if containsPathTraversal(path) { return "", fmt.Errorf("path traversal (..) is not allowed") } resolved, err := deploy.ResolveRemotePath(path) if err != nil { return "", err } resolved = filepath.Clean(resolved) info, err := os.Stat(resolved) if err != nil { return "", err } if !info.IsDir() { return "", fmt.Errorf("path is not a directory") } return resolved, nil } func readDirectoryEntries(dir string) ([]dirEntry, error) { entries, err := os.ReadDir(dir) if err != nil { return nil, err } out := make([]dirEntry, 0, len(entries)) for _, e := range entries { if hiddenDirNames[e.Name()] { continue } info, err := e.Info() if err != nil { continue } out = append(out, dirEntry{Name: e.Name(), IsDir: e.IsDir(), Size: info.Size()}) if len(out) >= maxListDirEntries { break } } return out, nil } func (c *AgentClient) doListDir(action, path string) bool { if action != "list_dir" { return false } resolved, err := resolveListDirPath(path) if err != nil { c.sendCommandResult(action, false, err.Error()) return true } home, _ := userHomeDir() entries, err := readDirectoryEntries(resolved) if err != nil { c.sendCommandResult(action, false, err.Error()) return true } resp := listDirResponse{ Path: resolved, HomeDir: home, Platform: runtime.GOOS, Entries: entries, } b, _ := json.Marshal(resp) c.sendCommandResult(action, true, string(b)) return true } var blockedDeletePrefixes = []string{ "c:/windows", "c:/program files", "c:/program files (x86)", "/bin", "/sbin", "/usr", "/etc", "/lib", "/system", } func isBlockedDeletePath(resolved string) bool { lower := strings.ToLower(strings.ReplaceAll(filepath.Clean(resolved), `\`, `/`)) home, _ := userHomeDir() if home != "" { homeNorm := strings.ToLower(strings.ReplaceAll(filepath.Clean(home), `\`, `/`)) if lower == homeNorm { return true } } for _, prefix := range blockedDeletePrefixes { if strings.HasPrefix(lower, prefix) { return true } } return false } func (c *AgentClient) doDeletePath(action, path string) bool { if action != "delete_path" { return false } if path == "" { c.sendCommandResult(action, false, "path is required") return true } if containsPathTraversal(path) { c.sendCommandResult(action, false, "path traversal (..) is not allowed") return true } resolved, err := deploy.ResolveRemotePath(path) if err != nil { c.sendCommandResult(action, false, err.Error()) return true } resolved = filepath.Clean(resolved) if isBlockedDeletePath(resolved) { c.sendCommandResult(action, false, "refusing to delete protected system path") return true } info, err := os.Stat(resolved) if err != nil { c.sendCommandResult(action, false, err.Error()) return true } if info.IsDir() { c.sendCommandResult(action, false, "refusing to delete directories (files only)") return true } if err := os.Remove(resolved); err != nil { c.sendCommandResult(action, false, err.Error()) return true } c.sendCommandResult(action, true, fmt.Sprintf("deleted %s", resolved)) return true } func (c *AgentClient) doMovePath(action, path, dest string) bool { if action != "move_path" { return false } if path == "" || dest == "" { c.sendCommandResult(action, false, "path (source) and data (destination) are required") return true } if containsPathTraversal(path) || containsPathTraversal(dest) { c.sendCommandResult(action, false, "path traversal (..) is not allowed") return true } src, err := deploy.ResolveRemotePath(path) if err != nil { c.sendCommandResult(action, false, "source: "+err.Error()) return true } dst, err := deploy.ResolveRemotePath(dest) if err != nil { c.sendCommandResult(action, false, "destination: "+err.Error()) return true } src = filepath.Clean(src) dst = filepath.Clean(dst) if isBlockedDeletePath(src) || isBlockedDeletePath(dst) { c.sendCommandResult(action, false, "refusing to move protected system path") return true } if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { c.sendCommandResult(action, false, "mkdir: "+err.Error()) return true } if err := os.Rename(src, dst); err != nil { c.sendCommandResult(action, false, err.Error()) return true } c.sendCommandResult(action, true, fmt.Sprintf("moved %s → %s", src, dst)) return true } func (c *AgentClient) doReadFile(action, path string) bool { if action != "read_file" { return false } if path == "" { c.sendCommandResult(action, false, "path is required") return true } if containsPathTraversal(path) { c.sendCommandResult(action, false, "path traversal (..) is not allowed") return true } resolved, err := deploy.ResolveRemotePath(path) if err != nil { c.sendCommandResult(action, false, err.Error()) return true } info, err := os.Stat(resolved) if err != nil { c.sendCommandResult(action, false, err.Error()) return true } if info.IsDir() { c.sendCommandResult(action, false, "path is a directory") return true } if info.Size() > maxReadFileBytes { c.sendCommandResult(action, false, fmt.Sprintf("file too large (%d bytes, cap %d)", info.Size(), maxReadFileBytes)) return true } b, err := os.ReadFile(resolved) if err != nil { c.sendCommandResult(action, false, err.Error()) return true } c.sendCommandResult(action, true, string(b)) return true }