# Creative: Runtime Logs Path Safety (CP-4) 📌 **CREATIVE PHASE START: Filesystem Path Hardening** ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ## 1️⃣ PROBLEM **Description:** HTTP API принимает `{filename}` и читает/удаляет файлы под `EVOBGP_RUNTIME_LOGS_DIR`. Без жёсткой политики возможны path traversal, чтение произвольных файлов при symlink attack, доступ к не-log артефактам. **Requirements:** - Только файлы внутри configured root directory - Только basenames из list API (no subdirs) - Имена как у sidecar: `postgres.log`, `evobgp-all.log`, … - Fail closed при любой аномалии **Constraints:** - Root: `/opt/evobgp/runtime-logs` (prod) или `./runtime-logs` (dev) - API disabled если root unset или not `evobgp-all` ## 2️⃣ OPTIONS | Option | Описание | |--------|----------| | **A** | Strict basename allowlist regex + `filepath.Join(root, base)` + prefix check | | **B** | Только list-then-operate: handler хранит cache allowed names из ListDir | | **C** | Regex only, без EvalSymlinks | | **D** | Regex + EvalSymlinks + `os.SameFile` root check | ## 3️⃣ ANALYSIS | Criterion | A | B | C | D | |-----------|---|---|---|---| | Traversal resistance | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | | Symlink safety | ⭐⭐⭐ | ⭐⭐⭐ | ⭐ | ⭐⭐⭐⭐⭐ | | Simplicity | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | | No TOCTOU list cache | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ## 4️⃣ DECISION **Selected: Option D** — regex + resolved path under root. ### Filename allowlist ```regexp ^[a-z0-9][a-z0-9_.-]*\.log$ ``` - Длина: 3–128 - Запрещено: `..`, `/`, `\`, null - Примеры valid: `evobgp-all.log`, `postgres.log`, `bird2.log` - URL path param: только unescaped basename; handler rejects `%2e%2e` ### Resolution algorithm (`SafePath(root, filename)`) 1. Reject if `filename != filepath.Base(filename)` or fails regex 2. `candidate := filepath.Join(root, filename)` 3. `resolved, err := filepath.EvalSymlinks(candidate)` — if not exist for new file, use `filepath.Clean(candidate)` for delete target that exists 4. `rootAbs := filepath.Clean(root)` (must be absolute after config load) 5. Require `strings.HasPrefix(resolved+string(os.PathSeparator), rootAbs+string(os.PathSeparator))` OR `resolved == rootAbs` (reject) 6. Reject if `resolved` is directory ### ListDir - `os.ReadDir(root)` only — **no recursion** - Skip subdirectories, non-matching names, hidden files (prefix `.`) - Return only entries passing regex ### Guard (`Enabled()`) ```text EVOBGP_RUNTIME_LOGS_DIR != "" AND filepath.IsAbs(dir) OR dir cleaned to absolute at startup AND EVOBGP_SERVICE == "evobgp-all" AND os.Stat(root) is directory ``` Otherwise handlers return `503` type `runtime_logs_unavailable`. ### Config load - `EVOBGP_RUNTIME_LOGS_DIR` trimmed; default empty (disabled) - At bootstrap: `filepath.Abs(dir)`; log warning if not exists (list returns empty, not error) ## 5️⃣ IMPLEMENTATION NOTES - Package: `internal/runtimelogs/safe.go` — `ValidateFilename`, `ResolveLogPath` - Tests: `../../../etc/passwd`, `foo/../bar.log`, symlink escape (skip on Windows if needed), valid names - OpenAPI `filename` path param pattern + description - **Never** accept absolute paths or globs from client - List response `name` field = basename only; UI passes same string back ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 📌 **CREATIVE PHASE END: Path Safety**