Files
EvoBGP/memory-bank/creative/creative-runtime-logs-path-safety.md
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

94 lines
3.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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$
```
- Длина: 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()`)
```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**