Skip to content

fix(security): apply 0600 permissions on memory plugin config files containing API keys#32244

Closed
AhmetArif0 wants to merge 1 commit into
NousResearch:mainfrom
AhmetArif0:fix/memory-plugin-config-permissions
Closed

fix(security): apply 0600 permissions on memory plugin config files containing API keys#32244
AhmetArif0 wants to merge 1 commit into
NousResearch:mainfrom
AhmetArif0:fix/memory-plugin-config-permissions

Conversation

@AhmetArif0

Copy link
Copy Markdown
Contributor

Summary

Four memory plugin save_config() methods write JSON config files via
write_text(), which obeys the process umask (typically 0o022), leaving
them world-readable (0o644) on shared hosts:

File Secret stored
~/.hermes/honcho.json Honcho API key ("secret": True)
~/.hermes/mem0.json Mem0 Platform API key ("secret": True, required)
~/.hermes/hindsight/config.json Hindsight Cloud API key ("secret": True)
~/.hermes/supermemory.json Supermemory API key ("secret": True, required)

A local user on a multi-user host could read these files and recover live API keys.

Root cause

# all four plugins — same pattern
config_path.write_text(json.dumps(existing, indent=2))
# ↑ obeys umask → 0o644 by default; no chmod follows

Fix

Add os.chmod(config_path, 0o600) immediately after write_text() at
each of the four save_config() sites. honcho/__init__.py also adds a
local import os inside the method because that module has no top-level
os import.

Matches the pattern from the recent security cluster:

  • 79fc92e9c.env creation sites
  • 3bace071bwebhook_subscriptions.json, response_store.db
  • 782681f90 — Google Chat OAuth credentials

Test plan

  • One test_save_config_sets_owner_only_permissions regression test added per plugin, asserting the resulting file mode is 0o600 under a permissive umask (skipped on Windows where POSIX mode bits are not enforced)
  • 414 / 414 tests passed across all five affected test suites
  • ruff check clean on all changed files

…ontaining API keys

Four memory plugin save_config() methods wrote their JSON config files via
write_text(), which obeys the process umask (typically 0o022), leaving
them at 0644 — world-readable on shared hosts:

  ~/.hermes/honcho.json       — Honcho API key
  ~/.hermes/mem0.json         — Mem0 Platform API key (required)
  ~/.hermes/hindsight/config.json — Hindsight Cloud API key
  ~/.hermes/supermemory.json  — Supermemory API key (required)

All four schemas declare api_key with "secret": True. A local user on a
multi-user host could read these files and recover live API keys.

Apply os.chmod(config_path, 0o600) immediately after write_text() at
each of the four save_config() call sites, matching the pattern used by
the recent security cluster (79fc92e for .env, 3bace07 for
webhook_subscriptions.json, 782681f for Google Chat OAuth).

honcho/__init__.py also adds a local `import os` inside save_config()
because that module does not import os at the top level.

Adds one regression test per plugin asserting that the resulting file
mode is 0o600 under a permissive umask (skipped on Windows).

@hclsys hclsys left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Real issue and the right files, but the fix has a TOCTOU window — and notably the codebase already has the correct pattern for exactly this case.

write_text() creates the file using the process umask first (commonly 0o644 = world-readable), and only then does os.chmod(config_path, 0o600). On the multi-user host that is this PR's stated threat model, there's a window between the write and the chmod where the API-key file exists at 0o644 — a local attacker polling the path can read the key before the chmod lands. So the fix narrows the exposure but doesn't fully close it.

This exact problem (and the fix) is already established in the repo for the same class of secret:

  • agent/google_oauth.py:501-502: "Create with 0o600 atomically to close the TOCTOU window where the default umask (often 0o644) would briefly expose tokens" — uses os.open(path, os.O_CREAT|os.O_RDWR, 0o600) (:181).
  • agent/anthropic_adapter.py:1049-1052: same documented atomic-0600-create rationale.
  • agent/secret_sources/bitwarden.py:154: chmods the temp file 0600 before the atomic rename, so the final path never exists world-readable.

Suggest matching one of those instead of chmod-after-write — e.g. write to a temp file created with os.open(..., 0o600) (or NamedTemporaryFile + os.chmod before close) and os.replace() onto the final path. That closes the window and also makes the config write atomic (no torn config on crash mid-write). Even simpler if utils exposes an atomic-secure-write helper (onboarding uses atomic_yaml_write at agent/onboarding.py:174) — worth checking whether it can be reused here.

Minor: confirm honcho/__init__.py has os in scope at the chmod site — it has no module-level import os; the PR adds a local import os in that method, so just make sure the chmod and the import are in the same function.

The intent is right; just want the final file to never be world-readable, not "world-readable then fixed."

@alt-glitch alt-glitch added type/security Security vulnerability or hardening P2 Medium — degraded but workaround exists comp/plugins Plugin system and bundled plugins tool/memory Memory tool and memory providers labels May 25, 2026
kshitijk4poor pushed a commit that referenced this pull request May 30, 2026
Self-hosted Honcho setup had four sharp edges:

- local/cloud URLs ending in /vN double-prefixed by the SDK (/v3/v3/... 404)
- authenticated local servers had no setup prompt for a JWT/bearer token
- profile-derived host keys could be dot-containing workspace IDs Honcho rejects
- memory-provider config files with API keys written world-readable per umask

This keeps existing behavior but makes those paths safer:

- strip a trailing /vN version segment from any configured baseUrl before SDK
  init (the SDK's route builders always prepend their own version prefix);
  auth-skipping stays loopback-only
- add an optional local JWT/bearer prompt in honcho setup, stored under
  hosts.<host>.apiKey
- derive new profile host keys with underscores, still reading legacy
  hermes.<profile> blocks
- write memory-provider config files atomically with 0600 via a shared
  utils.atomic_json_write(mode=) arg (honcho/hindsight/mem0/supermemory)
- skip honcho.json parsing in gateway cache-busting unless Honcho is the active
  memory provider; memoize by honcho.json mtime when active
- bust the gateway agent cache on memory.provider change
- add a hermes memory setup <provider> one-liner so fresh installs can configure
  a named provider without the picker (the per-provider hermes <provider>
  subcommand only registers once that provider is active)

Closes #20688, #29885, #26459, #30246, #33382, #32244.

Co-authored-by: BROCCOLO1D
sradetzky pushed a commit to sradetzky/hermes-agent that referenced this pull request May 30, 2026
Self-hosted Honcho setup had four sharp edges:

- local/cloud URLs ending in /vN double-prefixed by the SDK (/v3/v3/... 404)
- authenticated local servers had no setup prompt for a JWT/bearer token
- profile-derived host keys could be dot-containing workspace IDs Honcho rejects
- memory-provider config files with API keys written world-readable per umask

This keeps existing behavior but makes those paths safer:

- strip a trailing /vN version segment from any configured baseUrl before SDK
  init (the SDK's route builders always prepend their own version prefix);
  auth-skipping stays loopback-only
- add an optional local JWT/bearer prompt in honcho setup, stored under
  hosts.<host>.apiKey
- derive new profile host keys with underscores, still reading legacy
  hermes.<profile> blocks
- write memory-provider config files atomically with 0600 via a shared
  utils.atomic_json_write(mode=) arg (honcho/hindsight/mem0/supermemory)
- skip honcho.json parsing in gateway cache-busting unless Honcho is the active
  memory provider; memoize by honcho.json mtime when active
- bust the gateway agent cache on memory.provider change
- add a hermes memory setup <provider> one-liner so fresh installs can configure
  a named provider without the picker (the per-provider hermes <provider>
  subcommand only registers once that provider is active)

Closes NousResearch#20688, NousResearch#29885, NousResearch#26459, NousResearch#30246, NousResearch#33382, NousResearch#32244.

Co-authored-by: BROCCOLO1D
KKT-OPT pushed a commit to KKT-OPT/hermes-agent that referenced this pull request May 31, 2026
Self-hosted Honcho setup had four sharp edges:

- local/cloud URLs ending in /vN double-prefixed by the SDK (/v3/v3/... 404)
- authenticated local servers had no setup prompt for a JWT/bearer token
- profile-derived host keys could be dot-containing workspace IDs Honcho rejects
- memory-provider config files with API keys written world-readable per umask

This keeps existing behavior but makes those paths safer:

- strip a trailing /vN version segment from any configured baseUrl before SDK
  init (the SDK's route builders always prepend their own version prefix);
  auth-skipping stays loopback-only
- add an optional local JWT/bearer prompt in honcho setup, stored under
  hosts.<host>.apiKey
- derive new profile host keys with underscores, still reading legacy
  hermes.<profile> blocks
- write memory-provider config files atomically with 0600 via a shared
  utils.atomic_json_write(mode=) arg (honcho/hindsight/mem0/supermemory)
- skip honcho.json parsing in gateway cache-busting unless Honcho is the active
  memory provider; memoize by honcho.json mtime when active
- bust the gateway agent cache on memory.provider change
- add a hermes memory setup <provider> one-liner so fresh installs can configure
  a named provider without the picker (the per-provider hermes <provider>
  subcommand only registers once that provider is active)

Closes NousResearch#20688, NousResearch#29885, NousResearch#26459, NousResearch#30246, NousResearch#33382, NousResearch#32244.

Co-authored-by: BROCCOLO1D
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/plugins Plugin system and bundled plugins P2 Medium — degraded but workaround exists tool/memory Memory tool and memory providers type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants