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 } }