- 删除未接线的 exec.FileSandbox(含 5 个用例):文件沙箱从此只有 plugins/tools/io/file.go 一处实现 - io 侧取更严一侧:NUL 拒绝(原 io 缺)、写前按最深存在祖先判定落点(避免越界建目录)、roots 归化到真实路径(修 roots 自身是 symlink 时的全误拒) - exec/sandbox.go 保留并注释说明:那是**终端工作目录守卫**的原语(terminal.go 在用),不是文件沙箱 - 修假阳性:原 NUL 用例断言错误文本含 "NUL",而 t.TempDir() 目录名自带测试函数名 "…RejectsNULBytePath…"、错误又内嵌整条临时路径 ⇒ 删掉安全检查用例照样绿。改为哨兵错误 ErrNullByte + errors.Is(错误文本逐字节不变),修复后同一变异必红 - 同类排查:file_test.go 三处文本断言判定无风险(含空格的串不可能出现在临时目录名里),保留
111 lines
3.2 KiB
Go
111 lines
3.2 KiB
Go
package exec
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// 本文件是终端沙箱的路径收敛原语(roots 清洗、词法包含判定、symlink 逃逸解析),
|
|
// 由 terminal.go 用于工作目录守卫。**它不是文件沙箱**:文件读写的沙箱已按裁决
|
|
// 13.4.4 收敛到 plugins/tools/io 文件工具(design/04 §4)。两侧保持同一收敛口径:
|
|
// 词法 roots 包含 + 最深存在祖先的 symlink 解析 + NUL 拒绝。
|
|
|
|
// absCleaners returns a slice of absolute, clean paths from raw roots. Dirty or
|
|
// empty entries are dropped; non-absolute roots are made absolute against the
|
|
// process working directory.
|
|
func absCleaners(raw []string) []string {
|
|
out := make([]string, 0, len(raw))
|
|
for _, r := range raw {
|
|
r = strings.TrimSpace(r)
|
|
if r == "" {
|
|
continue
|
|
}
|
|
if !filepath.IsAbs(r) {
|
|
r = filepath.Join(".", r)
|
|
}
|
|
out = append(out, filepath.Clean(r))
|
|
}
|
|
return out
|
|
}
|
|
|
|
// withinAnyRoot reports whether path is lexically confined to one of roots.
|
|
// Containment is judged by filepath.Rel: the path is inside a root when the
|
|
// relative path is "." or does not begin with "..".
|
|
func withinAnyRoot(path string, roots []string) bool {
|
|
for _, root := range roots {
|
|
rel, err := filepath.Rel(root, path)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if rel == "." || (rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator))) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// resolveReal returns the real (symlink-free) path for p, resolving the deepest
|
|
// existing ancestor when p itself does not exist (for example a file about to be
|
|
// created). This prevents a symlink inside a root from pointing at a location
|
|
// outside it.
|
|
func resolveReal(p string) (string, error) {
|
|
cur := filepath.Clean(p)
|
|
remaining := ""
|
|
for {
|
|
if real, err := filepath.EvalSymlinks(cur); err == nil {
|
|
if remaining == "" {
|
|
return real, nil
|
|
}
|
|
return filepath.Join(real, remaining), nil
|
|
}
|
|
base := filepath.Base(cur)
|
|
parent := filepath.Dir(cur)
|
|
if parent == cur {
|
|
return "", fmt.Errorf("no existing ancestor for %s", p)
|
|
}
|
|
if remaining == "" {
|
|
remaining = base
|
|
} else {
|
|
remaining = filepath.Join(base, remaining)
|
|
}
|
|
cur = parent
|
|
}
|
|
}
|
|
|
|
// resolveInsideRoot validates a caller-supplied path and returns its absolute,
|
|
// symlink-resolved path, guaranteeing that both the lexical path and its real
|
|
// target stay inside one of roots. A relative path is resolved against
|
|
// roots[0]. The returned path is absolute and clean.
|
|
func resolveInsideRoot(candidate string, roots []string) (string, error) {
|
|
if candidate == "" {
|
|
return "", ErrEmptyPath
|
|
}
|
|
if strings.IndexByte(candidate, 0) >= 0 {
|
|
return "", ErrNullByte
|
|
}
|
|
|
|
var abs string
|
|
if filepath.IsAbs(candidate) {
|
|
abs = filepath.Clean(candidate)
|
|
} else {
|
|
if len(roots) == 0 {
|
|
return "", ErrNoAllowedRoot
|
|
}
|
|
abs = filepath.Join(roots[0], candidate)
|
|
}
|
|
|
|
if !withinAnyRoot(abs, roots) {
|
|
return "", fmt.Errorf("%w: %s", ErrPathOutsideRoot, candidate)
|
|
}
|
|
|
|
real, err := resolveReal(abs)
|
|
if err != nil {
|
|
return "", fmt.Errorf("%w: %s: %w", ErrPathOutsideRoot, candidate, err)
|
|
}
|
|
if !withinAnyRoot(real, roots) {
|
|
return "", fmt.Errorf("%w: %s (real target %s)", ErrSymlinkEscape, candidate, real)
|
|
}
|
|
return real, nil
|
|
}
|