fix: harden codex home boundaries

This commit is contained in:
Yoilun
2026-05-25 16:21:25 +08:00
parent be466ae5fc
commit dc8b06f961
6 changed files with 190 additions and 1 deletions

View File

@@ -2,6 +2,7 @@ package codexhome
import (
"errors"
"os"
"path/filepath"
"strings"
)
@@ -31,9 +32,22 @@ func ResolveInside(home string, rel string) (string, error) {
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 {
@@ -47,9 +61,73 @@ func IsForbidden(path string, home string) bool {
if err != nil {
return true
}
rel = filepath.ToSlash(rel)
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
}
}
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)))
}