7c7f6d7826
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).
84 lines
2.6 KiB
Go
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
|
|
}
|
|
}
|