137 lines
3.2 KiB
Go
137 lines
3.2 KiB
Go
package codexhome
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
var ErrOutsideCodexHome = errors.New("路径超出 Codex home")
|
|
var ErrForbiddenPath = errors.New("禁止访问敏感路径")
|
|
|
|
func ResolveInside(home string, rel string) (string, error) {
|
|
if filepath.IsAbs(rel) {
|
|
return "", ErrOutsideCodexHome
|
|
}
|
|
cleanHome, err := filepath.Abs(home)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
candidate, err := filepath.Abs(filepath.Join(cleanHome, rel))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
relative, err := filepath.Rel(cleanHome, candidate)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
|
|
return "", ErrOutsideCodexHome
|
|
}
|
|
if IsForbidden(candidate, cleanHome) {
|
|
return "", ErrForbiddenPath
|
|
}
|
|
if err := rejectSymlinkEscape(cleanHome, candidate); err != nil {
|
|
return "", err
|
|
}
|
|
return candidate, nil
|
|
}
|
|
|
|
func ResolveAgentTOML(home string, fileName string) (string, error) {
|
|
if filepath.IsAbs(fileName) || filepath.Dir(fileName) != "." || fileName == "" {
|
|
return "", ErrOutsideCodexHome
|
|
}
|
|
if !strings.HasSuffix(strings.ToLower(fileName), ".toml") {
|
|
return "", ErrForbiddenPath
|
|
}
|
|
return ResolveInside(home, filepath.Join("agents", fileName))
|
|
}
|
|
|
|
func IsForbidden(path string, home string) bool {
|
|
cleanHome, err := filepath.Abs(home)
|
|
if err != nil {
|
|
return true
|
|
}
|
|
cleanPath, err := filepath.Abs(path)
|
|
if err != nil {
|
|
return true
|
|
}
|
|
rel, err := filepath.Rel(cleanHome, cleanPath)
|
|
if err != nil {
|
|
return true
|
|
}
|
|
rel = strings.ToLower(filepath.ToSlash(rel))
|
|
forbidden := map[string]bool{
|
|
"auth.json": true,
|
|
}
|
|
return forbidden[rel]
|
|
}
|
|
|
|
func rejectSymlinkEscape(home string, candidate string) error {
|
|
evaluatedHome, err := evalExistingPath(home)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rel, err := filepath.Rel(home, candidate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if rel == "." {
|
|
return nil
|
|
}
|
|
currentLexical := home
|
|
currentEvaluated := evaluatedHome
|
|
for _, part := range strings.Split(rel, string(filepath.Separator)) {
|
|
currentLexical = filepath.Join(currentLexical, part)
|
|
currentEvaluated = filepath.Join(currentEvaluated, part)
|
|
info, err := os.Lstat(currentLexical)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 {
|
|
currentEvaluated, err = filepath.EvalSymlinks(currentLexical)
|
|
if err != nil {
|
|
return ErrOutsideCodexHome
|
|
}
|
|
}
|
|
if !isInside(evaluatedHome, currentEvaluated) {
|
|
return ErrOutsideCodexHome
|
|
}
|
|
if IsForbidden(currentEvaluated, evaluatedHome) {
|
|
return ErrForbiddenPath
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func evalExistingPath(path string) (string, error) {
|
|
evaluated, err := filepath.EvalSymlinks(path)
|
|
if err == nil {
|
|
return evaluated, nil
|
|
}
|
|
if !os.IsNotExist(err) {
|
|
return "", err
|
|
}
|
|
parent := filepath.Dir(path)
|
|
if parent == path {
|
|
return filepath.Abs(path)
|
|
}
|
|
evaluatedParent, err := evalExistingPath(parent)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(evaluatedParent, filepath.Base(path)), nil
|
|
}
|
|
|
|
func isInside(home string, path string) bool {
|
|
rel, err := filepath.Rel(home, path)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return rel == "." || (rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)))
|
|
}
|