Files
agents_and_robots/tools/file/validate.go
T
egutierrez 7c7f6d7826 test: agregar tests completos para write_file, list_directory, append_file, delete_file
44 tests cubriendo todas las nuevas tools de archivos y la tool existente.

Tests por tool:
- write_file (11): crear archivo, ReadOnly, path fuera de allowed, contenido >1MB,
  crear dirs padre, sobreescribir, path traversal, symlink escape, deny-by-default
- list_directory (9): listado plano y recursivo, limite 500 entries, symlinks fuera
  de allowed, path traversal, deny-by-default, no-directorio, dir vacio
- append_file (11): append a existente, crear si no existe, ReadOnly, path fuera,
  limite 10MB total, path traversal, symlink escape, crear dirs padre
- delete_file (9): borrar archivo, rechazar directorios, ReadOnly, path fuera,
  path traversal, symlink escape, archivo inexistente, deny-by-default

Tambien corrige resolveReal() para resolver paths con multiples niveles de
directorios inexistentes (necesario para MkdirAll en write/append).
2026-04-09 00:21:30 +00:00

84 lines
2.6 KiB
Go

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 walks up the tree to find the deepest
// existing ancestor, resolves its symlinks, and appends the remaining segments.
// This prevents partial traversal attacks via symlinks in non-existent paths.
func resolveReal(path string) (string, error) {
real, err := filepath.EvalSymlinks(path)
if err == nil {
return filepath.Clean(real), nil
}
// Walk up to find the deepest existing ancestor.
cleaned := filepath.Clean(path)
var tail []string
cur := cleaned
for {
parent := filepath.Dir(cur)
tail = append([]string{filepath.Base(cur)}, tail...)
realParent, err := filepath.EvalSymlinks(parent)
if err == nil {
// Found an existing ancestor — rebuild the path.
result := realParent
for _, seg := range tail {
result = filepath.Join(result, seg)
}
return filepath.Clean(result), nil
}
if parent == cur {
// Reached the root without finding an existing ancestor.
return "", fmt.Errorf("cannot resolve any ancestor of %q", path)
}
cur = parent
}
}