security(file-safety): also write-deny <root>/.env when running under a profile (#15981)
build_write_denied_paths() resolved the protected ``.env`` via get_hermes_home(), which is profile-aware. When a profile is active HERMES_HOME points at ``<root>/profiles/<name>`` and ``hermes_home / ".env"`` expands to the *profile* env file only — the global ``<root>/.env`` is left off the deny list and a write_file call against it succeeds. Since the top-level .env supplies credentials inherited by every profile, this is a P0 credential-exfiltration / overwrite path. Add a parallel ``_hermes_root_path()`` helper that returns the Hermes root (via the existing ``get_default_hermes_root()`` constant) and include ``<root>/.env`` in the deny list alongside ``<active_profile>/.env``. Both paths now refuse write_file/patch regardless of profile state. The active HERMES_HOME .env entry is preserved so the protection in non-profile mode is unchanged. A regression test exercises the profile-active scenario by pointing HERMES_HOME at ``<tmp>/profiles/coder`` and asserting that ``<tmp>/.env`` is denied. Fixes #15981
This commit is contained in:
@@ -16,9 +16,19 @@ def _hermes_home_path() -> Path:
|
||||
return Path(os.path.expanduser("~/.hermes"))
|
||||
|
||||
|
||||
def _hermes_root_path() -> Path:
|
||||
"""Resolve the Hermes root dir (always the parent of any profile, never per-profile)."""
|
||||
try:
|
||||
from hermes_constants import get_default_hermes_root # local import to avoid cycles
|
||||
return get_default_hermes_root()
|
||||
except Exception:
|
||||
return Path(os.path.expanduser("~/.hermes"))
|
||||
|
||||
|
||||
def build_write_denied_paths(home: str) -> set[str]:
|
||||
"""Return exact sensitive paths that must never be written."""
|
||||
hermes_home = _hermes_home_path()
|
||||
hermes_root = _hermes_root_path()
|
||||
return {
|
||||
os.path.realpath(p)
|
||||
for p in [
|
||||
@@ -26,7 +36,11 @@ def build_write_denied_paths(home: str) -> set[str]:
|
||||
os.path.join(home, ".ssh", "id_rsa"),
|
||||
os.path.join(home, ".ssh", "id_ed25519"),
|
||||
os.path.join(home, ".ssh", "config"),
|
||||
# Active profile .env (or top-level .env when not in profile mode).
|
||||
str(hermes_home / ".env"),
|
||||
# Top-level .env, even when running under a profile — overwriting it
|
||||
# leaks credentials across every profile that inherits from root (#15981).
|
||||
str(hermes_root / ".env"),
|
||||
os.path.join(home, ".bashrc"),
|
||||
os.path.join(home, ".zshrc"),
|
||||
os.path.join(home, ".profile"),
|
||||
|
||||
Reference in New Issue
Block a user