diff --git a/CHANGELOG.md b/CHANGELOG.md index 93d92ef..1ab9cad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Hermes Web UI -- Changelog +## [v0.50.32] fix(sessions): validate session_id before deleting session files [SECURITY] (#409) + +`/api/session/delete` accepted arbitrary `session_id` values from the request body and built the delete path directly as `SESSION_DIR / f"{sid}.json"`. Because pathlib discards the prefix when `sid` is an absolute path, an attacker could supply `/tmp/victim` and cause the server to unlink `victim.json` outside the session store. Traversal-style values (`../../etc/target`) were also accepted. CVSS 8.1 High (AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H). + +- `api/routes.py`: validate `session_id` against `[0-9a-z_]+` allowlist (covers `uuid4().hex[:12]` WebUI IDs and `YYYYMMDD_HHMMSS_hex` CLI IDs) before path construction; resolve candidate path and enforce `path.relative_to(SESSION_DIR)` containment before unlinking; only invalidate session index on successful deletion path, not on rejected requests +- `tests/test_sprint3.py`: 2 new regression tests — absolute-path payload rejected and file preserved, traversal payload rejected and file preserved +- Original PR by @Hinotoi-agent (cherry-picked; branch was 4 commits behind master) +- 1041 tests total (up from 1039) + ## [v0.50.31] fix: delegate all live model fetching to agent's provider_model_ids() `_handle_live_models()` in `api/routes.py` previously maintained its own per-provider fetch logic and returned `not_supported` for Anthropic, Google, and Gemini. Now it delegates entirely to the agent's `hermes_cli.models.provider_model_ids()` — the single authoritative resolver — and `_fetchLiveModels()` in `ui.js` no longer skips any provider. diff --git a/api/routes.py b/api/routes.py index db1c1f8..5e72619 100644 --- a/api/routes.py +++ b/api/routes.py @@ -724,10 +724,16 @@ def handle_post(handler, parsed) -> bool: sid = body.get("session_id", "") if not sid: return bad(handler, "session_id is required") + if not all(c in '0123456789abcdefghijklmnopqrstuvwxyz_' for c in sid): + return bad(handler, "Invalid session_id", 400) # Delete from WebUI session store with LOCK: SESSIONS.pop(sid, None) - p = SESSION_DIR / f"{sid}.json" + try: + p = (SESSION_DIR / f"{sid}.json").resolve() + p.relative_to(SESSION_DIR.resolve()) + except Exception: + return bad(handler, "Invalid session_id", 400) try: p.unlink(missing_ok=True) except Exception: diff --git a/static/index.html b/static/index.html index f5fea4a..b69fa9d 100644 --- a/static/index.html +++ b/static/index.html @@ -535,7 +535,7 @@