mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-02 07:11:49 +00:00
fix(state): restrict sensitive store file permissions
response_store.db (api server) holds conversation history including tool payloads, prompts, and results. webhook_subscriptions.json holds per-route HMAC secrets. Under a permissive umask (e.g. 0o022, default on most distros) both files were created mode 0o644 — readable by other local users on shared boxes. - gateway/platforms/api_server.py: ResponseStore tightens itself + WAL/SHM sidecars to 0o600 after __init__, then trusts the inode. (Original contributor patch chmod'd after every _commit() — wasteful on a hot api_server path; chmod-on-create is sufficient since SQLite preserves mode bits across writes.) - hermes_cli/webhook.py: _save_subscriptions writes via tempfile.mkstemp (which itself creates the file with 0o600), chmods the temp before the atomic rename, and re-asserts 0o600 on the destination so an existing permissive file from before this fix gets narrowed. Tests cover (a) creation under permissive umask leaves 0o600 and (b) an existing 0o644 webhook_subscriptions.json gets narrowed on next save. Tests guarded with skipif os.name=='nt' since POSIX mode bits don't apply on Windows. Salvaged from PR #30917 by @Hinotoi-agent. Reworked the api_server.py side from chmod-on-every-commit to chmod-on-create. Co-authored-by: teknium1 <127238744+teknium1@users.noreply.github.com>
This commit is contained in:
parent
f378f00bfb
commit
3bace071bf
4 changed files with 116 additions and 5 deletions
|
|
@ -11,8 +11,10 @@ hot-reloaded by the webhook adapter without a gateway restart.
|
|||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
|
@ -23,6 +25,7 @@ from hermes_cli.config import cfg_get
|
|||
|
||||
|
||||
_SUBSCRIPTIONS_FILENAME = "webhook_subscriptions.json"
|
||||
_SUBSCRIPTIONS_FILE_MODE = 0o600
|
||||
|
||||
|
||||
def _hermes_home() -> Path:
|
||||
|
|
@ -48,12 +51,33 @@ def _load_subscriptions() -> Dict[str, dict]:
|
|||
def _save_subscriptions(subs: Dict[str, dict]) -> None:
|
||||
path = _subscriptions_path()
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp_path = path.with_suffix(".tmp")
|
||||
tmp_path.write_text(
|
||||
json.dumps(subs, indent=2, ensure_ascii=False),
|
||||
encoding="utf-8",
|
||||
# webhook_subscriptions.json contains per-route HMAC secrets — write
|
||||
# via tempfile + chmod 0o600 before the atomic rename so a permissive
|
||||
# umask cannot leave the secrets readable to other local users in the
|
||||
# window between create and rename.
|
||||
fd, tmp_name = tempfile.mkstemp(
|
||||
prefix=f".{path.name}.",
|
||||
suffix=".tmp",
|
||||
dir=path.parent,
|
||||
text=True,
|
||||
)
|
||||
atomic_replace(tmp_path, path)
|
||||
tmp_path = Path(tmp_name)
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
||||
json.dump(subs, fh, indent=2, ensure_ascii=False)
|
||||
fh.flush()
|
||||
os.fsync(fh.fileno())
|
||||
os.chmod(tmp_path, _SUBSCRIPTIONS_FILE_MODE)
|
||||
atomic_replace(tmp_path, path)
|
||||
# Re-assert after rename in case the destination existed with a
|
||||
# broader mode and atomic_replace preserved it.
|
||||
os.chmod(path, _SUBSCRIPTIONS_FILE_MODE)
|
||||
except Exception:
|
||||
try:
|
||||
tmp_path.unlink(missing_ok=True)
|
||||
except OSError:
|
||||
pass
|
||||
raise
|
||||
|
||||
|
||||
def _get_webhook_config() -> dict:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue