# pathsafe **Repository Path**: idcu-go/pathsafe ## Basic Information - **Project Name**: pathsafe - **Description**: No description available - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-08-29 - **Last Updated**: 2026-09-01 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # gitee.com/idcu-go/pathsafe > 当前版本:**v0.1.0** ## 1. 定位 + 当前版本 > 模块类型:**通用公共模块 · 工具 utility**(虚拟根 ↔ 真实根双向映射与路径穿越阻断,纯标准库)。 把宿主机上的一个真实目录当作「虚拟根」暴露给外部(LLM、Agent、HTTP 客户端)时,有两件事必须同时成立: 对外**隐藏真实布局**(少泄露信息、少占 token),对内**保证对方给回来的路径不会跳出根目录**。 本包把这两件事收敛成一个 `Root`:进去用 `Resolve`(虚拟→真实,越界即报错),出来用 `Virtualize` / `Remap`(真实→虚拟)。 只依赖标准库(`os` / `path` / `path/filepath` / `strings`),无 goroutine、无全局状态,可并发使用。 设计前提: - **越界一律报错,绝不静默夹紧**:静默夹紧会把「调用方算错了」伪装成成功返回; - **虚拟路径按 POSIX 语义解析**:虚拟根是给外部看的逻辑命名空间,不随运行平台变形态(Windows 上同样是 `/codebase/...`); - **只做路径映射与安全判定**:不碰权限位、不做文件读写(`EnsureDir` 是唯一的例外,且先过 `Resolve`)。 ## 2. 安装 ```bash go get gitee.com/idcu-go/pathsafe@v0.1.0 ``` **依赖**: - 内部(gitee.com/idcu-go/*):无 - 外部(第三方):无 - 用途:纯 stdlib 实现;仅需 `go 1.25` 与私有源 `GOPRIVATE="gitee.com/idcu-go/*"`。 --- ## 3. API 签名 包内导出符号共 13 个(1 个类型、3 个函数、3 个哨兵、6 个方法): ```go package pathsafe // import "gitee.com/idcu-go/pathsafe" // 虚拟根与真实根的绑定;零值不可用,须经 NewRoot 构造。 type Root struct{ /* ... */ } // 绑定真实根与虚拟根。realRoot 必须绝对路径;virtualRoot 为空时回退为 realRoot //(等价于只做越界校验、不做改写)。 func NewRoot(realRoot, virtualRoot string, opts ...Option) (*Root, error) // 函数式配置。 type Option func(*options) func WithResolveSymlinks(enabled bool) Option // 开启符号链接解析校验,默认关闭 func (r *Root) Real() string // 真实根 func (r *Root) Virtual() string // 虚拟根 func (r *Root) Resolve(virtual string) (string, error) // 虚拟 → 真实,越界报错 func (r *Root) Virtualize(real string) (string, error) // 真实 → 虚拟,根外报错 func (r *Root) Remap(text string) string // 文本中的真实根前缀 → 虚拟根(best effort) func (r *Root) Contains(real string) bool // 真实路径是否在根内 func (r *Root) EnsureDir(virtual string) (string, error) // 在根内安全创建目录(含父目录) // 包级便捷函数(无需构造 Root)。 func Join(root, untrusted string) (string, error) // 不可信相对路径安全拼到 root 下,越界报错 func Within(root, candidate string) bool // candidate 是否在 root 内(等于 root 也算) // 错误哨兵与判定。 var ( ErrEscape = errors.New("pathsafe: path escapes root") ErrInvalidRoot = errors.New("pathsafe: invalid root") ErrNotUnderVirtualRoot = errors.New("pathsafe: path not under virtual root") ) func IsEscape(err error) bool // 穿越类(安全审计与告警用) func IsInvalidInput(err error) bool // 用法类(根非法 / 虚拟前缀不对) ``` --- ## 4. 关键类型 / 结构体 本模块的公共入口是一个 `Root` 值类型;其字段不对外导出,语义通过「真实根 + 虚拟根」二元绑定表达: | 概念 | 语义 | |---|---| | 真实根(`Real()`) | 宿主机上实际落地目录的绝对路径,`NewRoot` 传入的第一参;所有安全判定以其为边界。 | | 虚拟根(`Virtual()`) | 对外暴露的逻辑命名空间,`NewRoot` 传入的第二参(空则回退为真实根);统一为 `/` 开头的 POSIX 形态。 | > `Option` 为函数式配置(当前 `WithResolveSymlinks`),`Join` / `Within` 为不依赖 `Root` 实例的包级便捷函数。 --- ## 5. 最小用法示例 ```go package main import ( "fmt" "os" "gitee.com/idcu-go/pathsafe" ) func main() { root, err := pathsafe.NewRoot("/srv/project", "/codebase") if err != nil { panic(err) } // 外部(LLM / Agent)给回来的虚拟路径 → 真实路径 real, err := root.Resolve("/codebase/src/auth/handler.go") if err != nil { fmt.Println("越界或被拒绝:", err) return } b, err := os.ReadFile(real) if err != nil { panic(err) } fmt.Println(len(b)) // 对外输出:真实路径 → 虚拟路径 v, _ := root.Virtualize(real) fmt.Println(v) // /codebase/src/auth/handler.go // 命令输出脱敏 fmt.Println(root.Remap("found in /srv/project/src/auth/handler.go:42")) // found in /codebase/src/auth/handler.go:42 } ``` ### 接入第二个领域(静态文件服务 / 网盘目录) 同一个 `Root` 换个虚拟根即可:把用户可见目录映射成 `/files`,下载接口的 `path` 参数过一遍 `Resolve`, 既隐藏了磁盘布局,也堵住了 `?path=../../etc/passwd`。 ```go files, _ := pathsafe.NewRoot("/var/lib/files", "/files") real, err := files.Resolve(r.URL.Query().Get("path")) if err != nil { http.Error(w, "bad path", http.StatusBadRequest) return } http.ServeFile(w, r, real) ``` --- ## 6. 工作机制 / 边界语义 ### Resolve 接受的三种入参 | 形态 | 例 | 结果 | |---|---|---| | 绝对虚拟路径 | `/codebase/src/a.go` | `/src/a.go` | | 相对路径 | `src/a.go` | `/src/a.go` | | 恰好等于虚拟根 / 空串 | `/codebase`、`""` | `` 本身 | 清洗后仍逃出真实根的一律返回 `ErrEscape`:`/codebase/../secret`、`../secret`、`a/../../secret`; 不是虚拟根前缀的绝对路径(如 `/etc/passwd`)返回 `ErrNotUnderVirtualRoot`(同样被 `IsEscape` 判定为穿越)。 ### 虚拟路径用 POSIX 语义 虚拟根统一为 `/` 开头的 POSIX 形态(`ToSlash` + 去结尾斜杠),前缀判定也按 `/` 做, 因此在 Windows 上 `/codebase/src/a.go` 依然被识别为绝对虚拟路径, 不会被 `filepath.IsAbs` 误判成相对路径。 ### 符号链接:默认按词法放行 `filepath.Clean` 挡不住「根内 symlink 指向根外」——路径形态完全合法,打开的却是别处的文件。 `WithResolveSymlinks(true)` 会对**已存在**的路径做 `EvalSymlinks` 并再校验一次是否在根内; 路径不存在时无法判定,跳过(真正打开时由操作系统裁决)。开启的代价是每次 `Resolve` 多一次系统调用。 ### Remap 是 best effort `Remap` 只做真实根前缀的字符串替换,未命中就原样返回,不报错——它用于给命令输出/错误文本脱敏, 不承担安全职责;安全由 `Resolve` 负责。 ### Join 不静默夹紧 `filepath.Join(root, "../../etc/passwd")` 会被静默夹到根内,这是很多路径穿越漏洞的来源。 本包的 `Join` 先清洗再判定:相对路径一旦出现 `..` 前缀即返回 `ErrEscape`。 ### 性能特征 ```bash go test -bench=. -benchmem ./... # 基准(解析 / 越界 / 虚拟化 / 文本替换 / Join) ``` 实测基准(Apple M 系列,go 1.25,`-benchmem`): | Benchmark | 说明 | 结果 | |---|---|---| | `BenchmarkResolveVirtual` | 虚拟 → 真实(正常路径) | **240.6 ns/op,40 B/op,2 allocs/op** | | `BenchmarkResolveEscape` | 越界被拦(含错误构造) | **1046 ns/op,288 B/op,9 allocs/op** | | `BenchmarkVirtualize` | 真实 → 虚拟 | **166.3 ns/op,40 B/op,2 allocs/op** | | `BenchmarkRemap` | 文本前缀替换 | **77.2 ns/op,48 B/op,1 allocs/op** | | `BenchmarkJoin` | 不可信路径安全拼接 | **229.0 ns/op,40 B/op,2 allocs/op** | > 正常路径 2 allocs 来自 `filepath.Join` + 返回字符串的拼接;越界路径要构造带上下文的错误, > 因此开销明显更高——但它本应是异常路径,不应出现在热路径上。 --- ## 7. 适用场景 - 为 AI / Agent / LLM 提供文件读写沙箱:用虚拟根收口外部返回的路径,防路径穿越(`?path=../../etc/passwd`)。 - 静态文件 / 网盘下载接口:把用户可见目录映射成虚拟根,隐藏磁盘布局并统一越界拒绝。 - 命令执行输出脱敏:用 `Remap` 把真实路径前缀替换成虚拟根,减少对外泄露。 - 需要「真实目录 ↔ 对外命名空间」双向映射且自带越界校验的通用工具场景。 ## 8. 变更记录 版本变更以仓库 git tag 为准,详见 tag **v0.1.0**(即当前版本)。本文件不在此手写版本清单,版本号须与最新 tag 保持一致。