package file import ( "fmt" "path/filepath" "strings" ) // validatePath checks that absPath is under one of the allowed paths. // Deny-by-default: if allowedPaths is empty, no paths are allowed. // Resolves symlinks to prevent traversal via ../ or symlink escapes. func validatePath(absPath string, allowedPaths []string) error { if len(allowedPaths) == 0 { return fmt.Errorf("file: no allowed paths configured, all operations denied") } // Resolve symlinks on the requested path to get the real path. // If the file doesn't exist yet, resolve the parent directory. realPath, err := resolveReal(absPath) if err != nil { return fmt.Errorf("file: cannot resolve path %q: %w", absPath, err) } for _, allowed := range allowedPaths { a, err := filepath.Abs(allowed) if err != nil { continue } // Resolve symlinks on the allowed path too. realAllowed, err := resolveReal(a) if err != nil { continue } // Ensure the real path is strictly under the allowed directory. // Add trailing separator to prevent /opt matching /opt1234. if strings.HasPrefix(realPath, realAllowed+string(filepath.Separator)) || realPath == realAllowed { return nil } } return fmt.Errorf("path %q not under any allowed path", absPath) } // validateWritePath checks path validity AND that writing is allowed. func validateWritePath(absPath string, allowedPaths []string, readOnly bool) error { if readOnly { return fmt.Errorf("file: write operations denied (read_only mode)") } return validatePath(absPath, allowedPaths) } // resolveReal resolves symlinks for a path. // If the exact path doesn't exist, it resolves the deepest existing ancestor // and appends the remaining segments, preventing partial traversal. func resolveReal(path string) (string, error) { real, err := filepath.EvalSymlinks(path) if err == nil { return filepath.Clean(real), nil } // Path doesn't exist — resolve parent and append base. parent := filepath.Dir(path) base := filepath.Base(path) realParent, err := filepath.EvalSymlinks(parent) if err != nil { return "", err } return filepath.Clean(filepath.Join(realParent, base)), nil }