Files
Cyrene_v2/internal/runtime/exec/sandbox.go
T

106 lines
2.8 KiB
Go

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
}