feat(exec): 本地执行 Provider + 放权终端沙箱(白名单/工作目录/超时/输出截断/默认关)+ 文件沙箱;config 增 ExecSandbox(默认关)
This commit is contained in:
1 parent
88dc12466d
commit
430fc4666b
11 files changed
+1199
-20
No files matched your search
@@ -0,0 +1,105 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
Reference in new issue
Block a user