From 4694524dee69ab67c294d284df8c351f396384f7 Mon Sep 17 00:00:00 2001 From: kronexoi Date: Fri, 22 May 2026 21:39:11 +0300 Subject: [PATCH] fix(security): restrict write access to Anthropic OAuth credential store --- agent/file_safety.py | 5 +++++ tests/tools/test_file_operations.py | 19 +++++++++++-------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/agent/file_safety.py b/agent/file_safety.py index efe7de4c183..e8c80f2b6d0 100644 --- a/agent/file_safety.py +++ b/agent/file_safety.py @@ -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"), diff --git a/tests/tools/test_file_operations.py b/tests/tools/test_file_operations.py index 1d3ec8b4a02..392e85d8956 100644 --- a/tests/tools/test_file_operations.py +++ b/tests/tools/test_file_operations.py @@ -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 /X and /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: # /profiles/coder/{auth.json,config.yaml,...}