Files
Agent-System/internal/engineeringdashboard/pathsafe.go
T
weichenandCursor a07ba1065b feat(engineering-dashboard): U2-U4 docs git and runtime collectors
Add allowlisted markdown indexing, read-only git plumbing via injected
runners, and docker/readiness collection with path and loopback guards.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 07:03:32 +00:00

110 lines
3.4 KiB
Go

package engineeringdashboard
import (
"fmt"
"os"
"path/filepath"
"strings"
)
var sensitivePathParts = []string{
"/.git/", "/.git", "/.env", "/.secrets/", "/.secrets", "/data/",
}
// ResolveRepoRoot returns an absolute, symlink-resolved git root.
func ResolveRepoRoot(repo string) (string, error) {
if repo == "" {
wd, err := os.Getwd()
if err != nil {
return "", err
}
repo = wd
}
abs, err := filepath.Abs(repo)
if err != nil {
return "", err
}
resolved, err := filepath.EvalSymlinks(abs)
if err != nil {
return "", err
}
fi, err := os.Stat(resolved)
if err != nil {
return "", err
}
if !fi.IsDir() {
return "", fmt.Errorf("repo root is not a directory: %s", resolved)
}
return resolved, nil
}
// SafeDocPath validates a repo-relative markdown path under .doc (or allowlisted root md).
func SafeDocPath(repoRoot, rel string) (abs string, err error) {
rel = filepath.ToSlash(rel)
rel = strings.TrimPrefix(rel, "./")
if rel == "" || strings.Contains(rel, "\x00") {
return "", fmt.Errorf("empty path")
}
if filepath.IsAbs(rel) || strings.HasPrefix(rel, "/") {
return "", fmt.Errorf("absolute path denied: %s", rel)
}
if strings.Contains(rel, "..") {
return "", fmt.Errorf("path traversal denied: %s", rel)
}
allowRootMD := !strings.Contains(rel, "/") && strings.HasSuffix(strings.ToLower(rel), ".md")
underDoc := strings.HasPrefix(rel, ".doc/")
if !underDoc && !allowRootMD {
return "", fmt.Errorf("path outside allowlist: %s", rel)
}
if strings.Contains(rel, "/assets/") || strings.HasSuffix(rel, "/assets") {
return "", fmt.Errorf("assets body denied: %s", rel)
}
joined := filepath.Join(repoRoot, filepath.FromSlash(rel))
// Reject if the path itself is a symlink (before resolution).
fi, err := os.Lstat(joined)
if err != nil {
return "", err
}
if fi.Mode()&os.ModeSymlink != 0 {
return "", fmt.Errorf("symlink denied: %s", rel)
}
resolved, err := filepath.EvalSymlinks(joined)
if err != nil {
return "", err
}
repoResolved, err := filepath.EvalSymlinks(repoRoot)
if err != nil {
repoResolved = repoRoot
}
if !strings.HasPrefix(resolved, repoResolved+string(os.PathSeparator)) && resolved != repoResolved {
return "", fmt.Errorf("resolved path escapes repo: %s", rel)
}
lower := filepath.ToSlash(strings.ToLower(resolved))
for _, part := range sensitivePathParts {
if strings.Contains(lower, part) || strings.HasSuffix(lower, strings.TrimSuffix(part, "/")) {
// allow .doc paths that happen to mention words; only block real sensitive dirs
if strings.Contains(lower, "/.git") || strings.Contains(lower, "/.env") ||
strings.Contains(lower, "/.secrets") || strings.Contains(lower, "/data/") {
if !strings.Contains(filepath.ToSlash(rel), ".doc/") {
return "", fmt.Errorf("sensitive path denied: %s", rel)
}
// still deny .git/.env/.secrets even under mistaken .doc
if strings.Contains(lower, "/.git") || strings.Contains(lower, "/.secrets") ||
strings.Contains(filepath.Base(lower), ".env") {
return "", fmt.Errorf("sensitive path denied: %s", rel)
}
}
}
}
if !strings.HasSuffix(strings.ToLower(resolved), ".md") {
return "", fmt.Errorf("extension not allowed: %s", rel)
}
return resolved, nil
}
// IsAssetsPath reports whether a relative path is under an assets directory.
func IsAssetsPath(rel string) bool {
rel = filepath.ToSlash(rel)
return strings.Contains(rel, "/assets/") || strings.HasSuffix(rel, "/assets") || strings.HasPrefix(rel, "assets/")
}