fix(security): restrict write access to Anthropic OAuth credential store

This commit is contained in:
kronexoi 2026-05-22 21:39:11 +03:00 committed by Teknium
parent be89c2e4fa
commit 4694524dee
2 changed files with 16 additions and 8 deletions

View file

@ -41,6 +41,11 @@ def build_write_denied_paths(home: str) -> set[str]:
# 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"),
# Active profile Anthropic PKCE credential store.
str(hermes_home / ".anthropic_oauth.json"),
# Top-level Anthropic PKCE credential store remains sensitive even
# when a profile is active; default/non-profile sessions still read it.
str(hermes_root / ".anthropic_oauth.json"),
os.path.join(home, ".bashrc"),
os.path.join(home, ".zshrc"),
os.path.join(home, ".profile"),

View file

@ -66,6 +66,7 @@ class TestIsWriteDenied:
"auth.json",
"config.yaml",
"webhook_subscriptions.json",
".anthropic_oauth.json",
"mcp-tokens/token1.json",
"mcp-tokens/subdir/token2.json",
"pairing/telegram-approved.json",
@ -74,8 +75,8 @@ class TestIsWriteDenied:
"pairing",
],
)
def test_hermes_control_files_and_mcp_tokens_denied(self, path):
"""Hermes control files and mcp-tokens/pairing entries must be write-denied."""
def test_hermes_control_files_oauth_and_mcp_tokens_denied(self, path):
"""Hermes control files, PKCE creds, mcp-tokens, and pairing entries must be write-denied."""
from hermes_constants import get_hermes_home
hermes_home = get_hermes_home()
full_path = str(hermes_home / path)
@ -86,11 +87,12 @@ class TestIsWriteDenied:
[
"dummy/../config.yaml",
"./auth.json",
"./.anthropic_oauth.json",
"mcp-tokens/../config.yaml",
],
)
def test_hermes_control_files_traversal_denied(self, path):
"""Path traversal attempts to control files must be blocked by realpath."""
def test_hermes_control_files_and_oauth_traversal_denied(self, path):
"""Path traversal attempts to protected Hermes files must be blocked."""
from hermes_constants import get_hermes_home
hermes_home = get_hermes_home()
full_path = str(hermes_home / path)
@ -110,14 +112,15 @@ class TestIsWriteDenied:
@pytest.mark.parametrize(
"name",
["auth.json", "config.yaml", "webhook_subscriptions.json"],
["auth.json", "config.yaml", "webhook_subscriptions.json", ".anthropic_oauth.json"],
)
def test_control_files_protected_in_profile_mode(self, tmp_path, monkeypatch, name):
def test_control_files_and_oauth_protected_in_profile_mode(self, tmp_path, monkeypatch, name):
"""Under a profile, BOTH <profile>/X and <root>/X must be denied (#15981 shape).
Without the root-level pass, a profile-mode session leaves the
global ~/.hermes/{auth.json,config.yaml,webhook_subscriptions.json}
writable the same gap PR #15981 fixed for .env.
global ~/.hermes/{auth.json,config.yaml,webhook_subscriptions.json,
.anthropic_oauth.json} writable the same gap PR #15981 fixed
for .env.
"""
# Simulate a profile-mode HERMES_HOME layout:
# <root>/profiles/coder/{auth.json,config.yaml,...}