mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-07-12 18:50:35 +00:00
Under a named profile, process HERMES_HOME is ~/.hermes/profiles/<name> but the allowlist still grants base ~/.hermes — so the prior deny (anchored only on the active-profile root + STATE_DIR) left ~/.hermes/state.db and sibling-profile secrets (~/.hermes/profiles/other/auth.json) reachable. Build deny roots from every Hermes state root the allowlist accepts: active HERMES_HOME, base ~/.hermes, api.profiles._DEFAULT_HERMES_HOME, and STATE_DIR; apply the state-subdir dir-denies under each. Widen the CSP-slice structural test window to match.
This commit is contained in:
+33
-10
@@ -8097,26 +8097,49 @@ def _handle_media(handler, parsed):
|
||||
".signing_key", ".pbkdf2_key", ".sessions.json",
|
||||
"google_token.json", "google_client_secret.json",
|
||||
}
|
||||
# Roots whose contents are sensitive in their entirety.
|
||||
# Roots whose contents are sensitive in their entirety. Cover EVERY Hermes
|
||||
# state root that the allowlist accepts — not just the active-profile
|
||||
# HERMES_HOME. Under a named profile the process HERMES_HOME is
|
||||
# ~/.hermes/profiles/<name>, but the allowlist (above) also grants the base
|
||||
# ~/.hermes, so without including the base root an attacker-influenced link
|
||||
# could still fetch ~/.hermes/state.db or a SIBLING profile's secrets
|
||||
# (~/.hermes/profiles/other/auth.json). (Codex review #3234.)
|
||||
_state_dir = None
|
||||
try:
|
||||
from api.config import STATE_DIR as _STATE_DIR
|
||||
_state_dir = Path(_STATE_DIR).resolve()
|
||||
except Exception:
|
||||
_state_dir = None
|
||||
_hermes_home_resolved = _HERMES_HOME.resolve()
|
||||
_deny_dirs = [d for d in (
|
||||
_base_hermes_home = None
|
||||
try:
|
||||
from api.profiles import _DEFAULT_HERMES_HOME as _BASE_HH
|
||||
_base_hermes_home = Path(_BASE_HH).resolve()
|
||||
except Exception:
|
||||
_base_hermes_home = None
|
||||
# All Hermes state roots: active-profile HERMES_HOME, base ~/.hermes,
|
||||
# api.profiles default home, and the WebUI STATE_DIR.
|
||||
_hermes_roots = []
|
||||
for _r in (
|
||||
_HERMES_HOME.resolve(),
|
||||
(_HOME / ".hermes").resolve(),
|
||||
_base_hermes_home,
|
||||
_state_dir,
|
||||
(_HERMES_HOME / "sessions").resolve(),
|
||||
(_HERMES_HOME / "memories").resolve(),
|
||||
(_HERMES_HOME / "profiles").resolve(),
|
||||
) if d is not None]
|
||||
):
|
||||
if _r is not None and _r not in _hermes_roots:
|
||||
_hermes_roots.append(_r)
|
||||
# Dir-based denies: the named state subdirs under every Hermes root, plus
|
||||
# the STATE_DIR itself (sensitive in its entirety).
|
||||
_deny_dirs = []
|
||||
if _state_dir is not None:
|
||||
_deny_dirs.append(_state_dir)
|
||||
for _root in _hermes_roots:
|
||||
for _sub in ("sessions", "memories", "profiles"):
|
||||
_deny_dirs.append((_root / _sub).resolve())
|
||||
# Filename-based denies only apply to files living under a Hermes/WebUI state
|
||||
# root — so a legitimate workspace or /tmp media artifact that happens to be
|
||||
# named settings.json / config.yaml is NOT blocked (Codex review #3234).
|
||||
_under_state_root = (
|
||||
_path_is_within_root(target, _hermes_home_resolved)
|
||||
or (_state_dir is not None and _path_is_within_root(target, _state_dir))
|
||||
_under_state_root = any(
|
||||
_path_is_within_root(target, _root) for _root in _hermes_roots
|
||||
)
|
||||
_denied = (
|
||||
(_under_state_root and target.name in _DENY_FILENAMES)
|
||||
|
||||
@@ -68,12 +68,13 @@ def test_html_media_open_full_uses_inline_new_tab_not_download():
|
||||
|
||||
def test_media_html_inline_keeps_csp_sandbox():
|
||||
"""api/media may serve HTML inline only behind a CSP sandbox."""
|
||||
# Slice widened to 7000 (was 5000) after the #3234 security deny-list block
|
||||
# was inserted earlier in _handle_media, pushing the CSP block further down
|
||||
# (the `csp=csp` line now sits ~6050 chars past the def). (Previously widened
|
||||
# Slice widened to 9000 (was 5000) after the #3234 security deny-list block
|
||||
# (multi-profile-aware deny roots) was inserted earlier in _handle_media,
|
||||
# pushing the CSP block to ~7800 chars past the def. (Previously widened
|
||||
# 4000→5000 for PR #2044's MEDIA_ALLOWED_ROOTS parsing.) The assertion is
|
||||
# structural, not positional.
|
||||
body = _slice_after(ROUTES_PY, "def _handle_media", 7000)
|
||||
# structural, not positional — generous headroom avoids re-breaking on small
|
||||
# future edits to the early part of _handle_media.
|
||||
body = _slice_after(ROUTES_PY, "def _handle_media", 9000)
|
||||
assert 'html_inline_ok = inline_preview and mime == "text/html"' in body
|
||||
assert 'csp = "sandbox allow-scripts" if html_inline_ok else None' in body
|
||||
assert "csp=csp" in body
|
||||
|
||||
Reference in New Issue
Block a user