// permission.go 实现 ACP fs/* method 的路径安全校验。 // // 规则(spec §5.6): // 1. 路径必须以 cwd_path 为前缀(防 ../../etc/passwd) // 2. 解析 symlink 后的真实路径必须仍在 cwd_path 下(防 symlink 逃逸) // // 三重校验:filepath.Abs + strings.HasPrefix + filepath.EvalSymlinks。 package acp import ( "path/filepath" "strings" "github.com/yan1h/agent-coding-workflow/internal/infra/errs" ) // ResolveSafePath 校验 requested 是否安全(在 cwd 内)。 // cwd 必须是绝对路径;requested 可以是绝对或相对(相对时基于 cwd 解析)。 // 返回校验通过后的绝对路径(含 symlink 解析);失败返回 CodeAcpFsPathOutsideWorktree。 func ResolveSafePath(cwd, requested string) (string, error) { if !filepath.IsAbs(cwd) { return "", errs.New(errs.CodeAcpFsPathOutsideWorktree, "cwd must be absolute") } cleanCwd := filepath.Clean(cwd) var abs string if filepath.IsAbs(requested) { abs = filepath.Clean(requested) } else { abs = filepath.Clean(filepath.Join(cleanCwd, requested)) } if !hasPathPrefix(abs, cleanCwd) { return "", errs.New(errs.CodeAcpFsPathOutsideWorktree, "path outside worktree") } real, err := filepath.EvalSymlinks(abs) if err == nil { if !hasPathPrefix(real, cleanCwd) { return "", errs.New(errs.CodeAcpFsPathOutsideWorktree, "path resolves outside worktree via symlink") } } return abs, nil } // hasPathPrefix reports whether abs is equal to base or under base. Trailing // separator handling is critical: we want "/foo/bar" to be inside "/foo" but // NOT inside "/fo". func hasPathPrefix(abs, base string) bool { if abs == base { return true } sep := string(filepath.Separator) return strings.HasPrefix(abs, base+sep) }