Решение: allowlist *.log, EvalSymlinks, проверка префикса root; creative phase завершён. Co-authored-by: Cursor <[email protected]>
3.7 KiB
3.7 KiB
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
^[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))
- Reject if
filename != filepath.Base(filename)or fails regex candidate := filepath.Join(root, filename)resolved, err := filepath.EvalSymlinks(candidate)— if not exist for new file, usefilepath.Clean(candidate)for delete target that existsrootAbs := filepath.Clean(root)(must be absolute after config load)- Require
strings.HasPrefix(resolved+string(os.PathSeparator), rootAbs+string(os.PathSeparator))ORresolved == rootAbs(reject) - Reject if
resolvedis directory
ListDir
os.ReadDir(root)only — no recursion- Skip subdirectories, non-matching names, hidden files (prefix
.) - Return only entries passing regex
Guard (Enabled())
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_DIRtrimmed; 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
filenamepath param pattern + description - Never accept absolute paths or globs from client
- List response
namefield = basename only; UI passes same string back
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 📌 CREATIVE PHASE END: Path Safety