Files
EvoBGP/memory-bank/creative/creative-runtime-logs-path-safety.md
T
DenozordecandCursor 1c39c65fc5 docs(memory-bank): add creative phase CP-4 path safety
Решение: allowlist *.log, EvalSymlinks, проверка префикса root; creative phase завершён.

Co-authored-by: Cursor <[email protected]>
2026-06-12 18:58:16 +07:00

3.7 KiB
Raw Blame History

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$
  • Длина: 3128
  • Запрещено: .., /, \, 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())

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.goValidateFilename, 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