fix(file_tools): reject '..' traversal in V4A patch headers

V4A patch '*** Update File:', '*** Add File:', '*** Delete File:' headers
come from patch CONTENT, not the explicit `path=` argument. That makes
them attacker-influenceable through skill content, web extract output,
prompt injection, and other surfaces the agent processes. Headers like
'*** Update File: ../../../etc/shadow' would resolve relative to the
agent's cwd; in deployment configurations where that cwd is deep enough
to land outside Hermes' protected paths, the write could land somewhere
the agent operator did not intend.

Reject any V4A header containing a '..' path component before applying
the patch. The explicit `path=` argument on patch_tool is UNCHANGED —
the agent legitimately uses '..' there (e.g. `patch path='../other_module/x.py'`
from a worktree dir is normal cross-module editing).

Regression tests: V4A Update header with traversal rejected, V4A Add
header with traversal rejected, patch_v4a never invoked when rejection
fires.

Salvaged from PR #29395 by @waefrebeorn. The original PR added
has_traversal_component as a blanket reject on read_file_tool,
write_file_tool, patch_tool's explicit path, and search_tool — that
would break legitimate agent operation where '..' is normal. Also
dropped the over-eager skills_guard pattern additions
(pickle.loads/marshal.loads/ctypes.CDLL/importlib at high/critical
severity would false-positive on legit data-science and FFI skills).

Co-authored-by: teknium1 <127238744+teknium1@users.noreply.github.com>
This commit is contained in:
waefrebeorn 2026-05-25 01:55:21 -07:00 committed by Teknium
parent 00bd24e27c
commit 5faea3f618
2 changed files with 56 additions and 1 deletions

View file

@ -211,6 +211,45 @@ class TestPatchHandler:
assert "error" in result
assert "Unknown mode" in result["error"]
@patch("tools.file_tools._get_file_ops")
def test_patch_v4a_rejects_traversal_in_update_header(self, mock_get):
"""V4A '*** Update File:' headers come from patch content, which can
carry prompt-injection-controlled paths (skill content, web extract).
``..`` traversal in the header must be rejected before the patch is
applied, even though the explicit ``path=`` arg is allowed to use
``..`` for legitimate cross-worktree edits."""
from tools.file_tools import patch_tool
result = json.loads(patch_tool(
mode="patch",
patch=(
"*** Begin Patch\n"
"*** Update File: ../../../etc/shadow\n"
"@@ -1,3 +1,3 @@\n"
"-old\n"
"+new\n"
"*** End Patch\n"
),
))
assert "error" in result
assert "traversal" in result["error"].lower()
# patch_v4a must not be invoked when the header is rejected
mock_get.return_value.patch_v4a.assert_not_called()
@patch("tools.file_tools._get_file_ops")
def test_patch_v4a_rejects_traversal_in_add_header(self, mock_get):
from tools.file_tools import patch_tool
result = json.loads(patch_tool(
mode="patch",
patch=(
"*** Begin Patch\n"
"*** Add File: ../../../tmp/dropped.py\n"
"+print('pwned')\n"
"*** End Patch\n"
),
))
assert "error" in result
assert "traversal" in result["error"].lower()
class TestSearchHandler:
@patch("tools.file_tools._get_file_ops")