mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
fix(file-safety): block auth.json read via TERMINAL_CWD relative path
read_file_tool resolves relative paths against TERMINAL_CWD (or the task's live terminal cwd), but the prior call passed the original unresolved string to get_read_block_error. That function's own resolve() is anchored at the Python process cwd, so when a task's TERMINAL_CWD pointed at HERMES_HOME and the agent issued read_file on the relative path "auth.json", the credential-store denylist was never reached and the file was read normally. Pass the already-resolved absolute path string at the file_tools call site, document the contract on get_read_block_error, and add a read_file_tool-level regression test that pins the relative-path case under TERMINAL_CWD == HERMES_HOME. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
056e00a77e
commit
567ea61298
3 changed files with 50 additions and 3 deletions
|
|
@ -136,7 +136,23 @@ def is_write_denied(path: str) -> bool:
|
|||
|
||||
|
||||
def get_read_block_error(path: str) -> Optional[str]:
|
||||
"""Return an error message when a read targets internal Hermes cache files."""
|
||||
"""Return an error message when a read targets a denied Hermes path.
|
||||
|
||||
Two categories are blocked:
|
||||
* Internal Hermes cache files under ``HERMES_HOME/skills/.hub`` —
|
||||
readable metadata that an attacker could use as a prompt-injection
|
||||
carrier.
|
||||
* Credential stores at the top of ``HERMES_HOME`` (``auth.json``,
|
||||
``auth.lock``, ``.anthropic_oauth.json``) — plaintext provider
|
||||
keys / OAuth tokens that the agent never needs to read directly.
|
||||
|
||||
Callers that resolve relative paths against a non-process cwd
|
||||
(e.g. ``TERMINAL_CWD`` in ``tools/file_tools.py``) MUST pre-resolve
|
||||
and pass the absolute path string. This function's own ``resolve()``
|
||||
is anchored at the Python process cwd, so a relative input like
|
||||
``"auth.json"`` would otherwise miss the denylist when the task's
|
||||
terminal cwd differs from the process cwd.
|
||||
"""
|
||||
resolved = Path(path).expanduser().resolve()
|
||||
hermes_home = _hermes_home_path().resolve()
|
||||
blocked_dirs = [
|
||||
|
|
|
|||
|
|
@ -121,3 +121,29 @@ def test_symlink_to_auth_json_blocked(fake_home, tmp_path):
|
|||
err = get_read_block_error(str(link))
|
||||
assert err is not None
|
||||
assert "credential store" in err
|
||||
|
||||
|
||||
def test_read_file_tool_blocks_relative_path_under_terminal_cwd(
|
||||
fake_home, tmp_path, monkeypatch
|
||||
):
|
||||
"""Bypass guard: a relative path like ``"auth.json"`` resolved by
|
||||
``read_file_tool`` against ``TERMINAL_CWD == HERMES_HOME`` must still
|
||||
be blocked, even though ``get_read_block_error``'s own ``resolve()``
|
||||
is anchored at the (different) Python process cwd.
|
||||
"""
|
||||
import json
|
||||
|
||||
import tools.file_tools as ft
|
||||
|
||||
_create(fake_home, "auth.json")
|
||||
# Force the file_tools resolver to anchor relative paths at HERMES_HOME
|
||||
# while the Python process cwd remains tmp_path (a different directory).
|
||||
monkeypatch.setenv("TERMINAL_CWD", str(fake_home))
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.setattr(
|
||||
ft, "_get_live_tracking_cwd", lambda task_id="default": None
|
||||
)
|
||||
|
||||
out = json.loads(ft.read_file_tool("auth.json"))
|
||||
assert "error" in out
|
||||
assert "credential store" in out["error"]
|
||||
|
|
|
|||
|
|
@ -474,8 +474,13 @@ def read_file_tool(path: str, offset: int = 1, limit: int = 500, task_id: str =
|
|||
})
|
||||
|
||||
# ── Hermes internal path guard ────────────────────────────────
|
||||
# Prevent prompt injection via catalog or hub metadata files.
|
||||
block_error = get_read_block_error(path)
|
||||
# Prevent prompt injection via catalog or hub metadata files,
|
||||
# and block credential stores under HERMES_HOME. Pass the
|
||||
# already-resolved path so a relative-path read against
|
||||
# TERMINAL_CWD == HERMES_HOME (e.g. "auth.json") still hits the
|
||||
# denylist — get_read_block_error's own resolve() runs against
|
||||
# the Python process cwd, which can differ.
|
||||
block_error = get_read_block_error(str(_resolved))
|
||||
if block_error:
|
||||
return json.dumps({"error": block_error})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue