fc644ecd6e
Reemplaza el scaffold del echobot por la plataforma completa de bots traida desde ~/DataProyects/Github/agents_and_robots tras la operacion Matrix-out: los bots ya no hablan por Matrix sino por el bus unibus (modelo todo-rooms + E2E via shell/transportunibus sobre github.com/enmanuel/unibus/pkg/client). - go.mod: replace de unibus -> ../unibus y de fn-registry -> ../../../.. (paths relativos reajustados a la nueva ubicacion dentro de fn_registry). - app.md: bump a 0.2.0, descripcion + arquitectura + comandos + gotchas reales. - modulo Go conservado como github.com/enmanuel/agents (sin reescribir imports). agents_and_robots queda archivado como museo de la era Matrix.
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
|
|
}
|
|
}
|