Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions gateway/platforms/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1096,12 +1096,15 @@ def _media_delivery_denied_paths() -> List[Path]:
# Bitwarden Secrets Manager plaintext disk cache.
os.path.join("cache", "bws_cache.json"),
)
# Directory trees whose every child is credential material. (MCP OAuth
# tokens under mcp-tokens/ are handled by the sibling targeted PR #37222;
# session/kanban SQLite stores by #41071 — kept out of this diff to avoid
# overlap.)
# Directory trees whose every child is credential material. ``mcp-tokens/``
# holds per-server MCP OAuth tokens; it mirrors the read guard
# (get_read_block_error) and the write guard (is_write_denied) in
# agent/file_safety.py, both of which already block the whole tree, so the
# delivery side must not trail them. (Session/kanban SQLite stores are
# handled by the sibling PR #41071 — kept out of this diff to avoid overlap.)
_ROOT_CREDENTIAL_DIRS = (
"pairing",
"mcp-tokens",
)
for hermes_root in (_HERMES_HOME, _HERMES_ROOT):
for rel in _ROOT_CREDENTIAL_FILES:
Expand Down
45 changes: 45 additions & 0 deletions tests/gateway/test_platform_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,29 @@ def test_recency_trust_denies_system_paths_even_when_fresh(self, tmp_path, monke

assert BasePlatformAdapter.validate_media_delivery_path(str(secret)) is None

def test_recency_trust_denies_mcp_tokens_even_when_fresh(self, tmp_path, monkeypatch):
"""MCP OAuth tokens under ~/.hermes/mcp-tokens/ are rewritten in place
on refresh, so their mtime is ~now on every turn — exactly the case the
recency window would otherwise re-trust (cf. the google_token.json
re-send exploit). The credential denylist must win over recency trust.
"""
self._patch_roots(monkeypatch)
monkeypatch.delenv("HERMES_MEDIA_ALLOW_DIRS", raising=False)
monkeypatch.setenv("HERMES_MEDIA_TRUST_RECENT_FILES", "1")
monkeypatch.setenv("HERMES_MEDIA_TRUST_RECENT_SECONDS", "600")

fake_home = tmp_path / "home"
hermes_dir = fake_home / ".hermes"
mcp_tokens = hermes_dir / "mcp-tokens"
mcp_tokens.mkdir(parents=True)
token = mcp_tokens / "github.json"
token.write_text('{"access_token": "gho_***"}') # mtime = now -> "recent"
monkeypatch.setenv("HOME", str(fake_home))
monkeypatch.setattr("gateway.platforms.base._HERMES_HOME", hermes_dir)
monkeypatch.setattr("gateway.platforms.base._HERMES_ROOT", hermes_dir)

assert BasePlatformAdapter.validate_media_delivery_path(str(token)) is None

def test_recency_trust_allows_pdf_in_project_dir(self, tmp_path, monkeypatch):
"""The motivating case: agent produces a PDF in a project directory.

Expand Down Expand Up @@ -1027,6 +1050,28 @@ def test_denylist_blocks_pairing_directory_contents(self, tmp_path, monkeypatch)

assert BasePlatformAdapter.validate_media_delivery_path(str(token)) is None

def test_denylist_blocks_mcp_tokens_directory_contents(self, tmp_path, monkeypatch):
"""Files under ~/.hermes/mcp-tokens/ hold per-server MCP OAuth tokens
and must not be deliverable. The read guard (get_read_block_error) and
the write guard (is_write_denied) in agent/file_safety.py already block
this tree; the delivery denylist mirrors them so an
``MEDIA:~/.hermes/mcp-tokens/github.json`` tag can't exfiltrate the
token JSON to a chat platform as a native attachment.
"""
self._patch_roots(monkeypatch)

fake_home = tmp_path / "home"
hermes_dir = fake_home / ".hermes"
mcp_tokens = hermes_dir / "mcp-tokens"
mcp_tokens.mkdir(parents=True)
token = mcp_tokens / "github.json"
token.write_text('{"access_token": "gho_***", "refresh_token": "ghr_***"}')
monkeypatch.setenv("HOME", str(fake_home))
monkeypatch.setattr("gateway.platforms.base._HERMES_HOME", hermes_dir)
monkeypatch.setattr("gateway.platforms.base._HERMES_ROOT", hermes_dir)

assert BasePlatformAdapter.validate_media_delivery_path(str(token)) is None

def test_hermes_cache_still_delivers_under_denied_home(self, tmp_path, monkeypatch):
"""The targeted credential denylist must not break legitimate cache
deliveries: a generated artifact under the allowlisted cache root is
Expand Down
Loading