docs(memory-bank): add creative phase CP-4 path safety
Решение: allowlist *.log, EvalSymlinks, проверка префикса root; creative phase завершён. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -2,25 +2,30 @@
|
||||
|
||||
## Текущий фокус
|
||||
|
||||
**VAN-инициализация** — Memory Bank создан, задача не определена.
|
||||
**Task:** `settings-ui-and-runtime-logs`
|
||||
**Complexity:** Level 4
|
||||
**Phase:** **CREATIVE complete** → **`/build`**
|
||||
|
||||
## Статус
|
||||
## Creative decisions (кратко)
|
||||
|
||||
- Memory Bank: создан и заполнен базовым контекстом проекта
|
||||
- Активная задача: отсутствует (ожидается описание от пользователя)
|
||||
| CP | Решение |
|
||||
|----|---------|
|
||||
| CP-1 | `/tenant-settings` + Tabs; mainNav «Параметры»; `/settings` = frontend |
|
||||
| CP-2 | Monitoring tab `runtime-logs`; sub-tabs `files` / `audit` |
|
||||
| CP-3 | Cleanup default `truncate`; max 512 MiB; tail 200 lines (max 2000), 256 KiB cap |
|
||||
| CP-4 | Regex `^[a-z0-9][a-z0-9_.-]*\.log$` + EvalSymlinks + root prefix |
|
||||
|
||||
## Наблюдаемый контекст (git)
|
||||
## Коммиты creative (local, no push)
|
||||
|
||||
Незакоммиченные файлы, вероятно из предыдущей сессии:
|
||||
|
||||
- `web/src/lib/components/monitoring/MaintenancePoliciesTab.svelte`
|
||||
- `web/src/lib/maintenance/policy-schedule.ts`
|
||||
|
||||
Область: UI политик обслуживания PostgreSQL (расписание cron, пресеты, CRUD).
|
||||
1. `ceb6f2f` — CP-1 tenant settings UI
|
||||
2. `e27936c` — CP-2 runtime logs UI
|
||||
3. `493575a` — CP-3 cleanup & tail
|
||||
4. (pending) — CP-4 path safety
|
||||
|
||||
## Следующий шаг
|
||||
|
||||
Определить задачу и уровень сложности (1–4), затем маршрутизация:
|
||||
```
|
||||
/build Phase 1
|
||||
```
|
||||
|
||||
- Level 1 → `/build`
|
||||
- Level 2–4 → `/plan`
|
||||
OpenAPI + migration `000026` + store (см. `tasks.md`).
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# 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**
|
||||
+16
-10
@@ -1,16 +1,22 @@
|
||||
# Memory Bank: Progress
|
||||
|
||||
## VAN Initialization — 2026-06-12
|
||||
## settings-ui-and-runtime-logs
|
||||
|
||||
| Шаг | Статус |
|
||||
|-----|--------|
|
||||
| Platform detection | ✅ Windows / PowerShell |
|
||||
| Memory Bank structure | ✅ Создан |
|
||||
| Core files populated | ✅ Базовый контекст EvoBGP |
|
||||
| Task analysis | ⏳ Ожидает описание задачи |
|
||||
| Complexity determination | ⏳ — |
|
||||
| Route to workflow | ⏳ — |
|
||||
| Фаза | Статус |
|
||||
|------|--------|
|
||||
| VAN | ✅ |
|
||||
| PLAN | ✅ |
|
||||
| CREATIVE | ✅ (4/4 docs, 4 commits) |
|
||||
| BUILD P1 OpenAPI+store | ⏳ |
|
||||
| BUILD P2 FS layer | ⏳ |
|
||||
| BUILD P3 HTTP | ⏳ |
|
||||
| BUILD P4 Deploy | ⏳ |
|
||||
| BUILD P5 Tenant UI | ⏳ |
|
||||
| BUILD P6 Runtime logs UI | ⏳ |
|
||||
| BUILD P7 QA | ⏳ |
|
||||
| REFLECT | ⏳ |
|
||||
| ARCHIVE | ⏳ |
|
||||
|
||||
## Реализация
|
||||
|
||||
_Нет активной реализации._
|
||||
_Не начата._
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|------|----------|
|
||||
| **Task ID** | `settings-ui-and-runtime-logs` |
|
||||
| **Complexity** | **Level 4** |
|
||||
| **Status** | **PLAN complete** → `/creative` |
|
||||
| **Status** | **CREATIVE complete** → `/build` |
|
||||
| **Дата VAN** | 2026-06-12 |
|
||||
| **Дата PLAN** | 2026-06-12 |
|
||||
|
||||
@@ -320,7 +320,7 @@ graph TD
|
||||
- [x] **CP-1 uiux** — `creative-tenant-settings-ui.md` ✅ Tabs `/tenant-settings`, nav «Параметры»
|
||||
- [x] **CP-2 uiux** — `creative-runtime-logs-ui.md` ✅ Monitoring tab `runtime-logs` + sub-tabs files/audit
|
||||
- [x] **CP-3 algorithm** — `creative-runtime-logs-cleanup.md` ✅ truncate default, max 512MiB, tail 200/2000 lines, 256KiB
|
||||
- [ ] **CP-4 architecture** — `creative-runtime-logs-path-safety.md`
|
||||
- [x] **CP-4 architecture** — `creative-runtime-logs-path-safety.md` ✅ regex + EvalSymlinks + root prefix check
|
||||
|
||||
---
|
||||
|
||||
@@ -340,7 +340,7 @@ graph TD
|
||||
|
||||
- [x] VAN
|
||||
- [x] PLAN
|
||||
- [ ] CREATIVE (4 docs)
|
||||
- [x] CREATIVE (4 docs)
|
||||
- [ ] BUILD Phase 1–7
|
||||
- [ ] REFLECT
|
||||
- [ ] ARCHIVE
|
||||
|
||||
Reference in New Issue
Block a user