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

@ -910,8 +910,24 @@ def patch_tool(mode: str = "replace", path: str = None, old_string: str = None,
_paths_to_check.append(path)
if mode == "patch" and patch:
import re as _re
from tools.path_security import has_traversal_component
for _m in _re.finditer(r'^\*\*\*\s+(?:Update|Add|Delete)\s+File:\s*(.+)$', patch, _re.MULTILINE):
_paths_to_check.append(_m.group(1).strip())
v4a_path = _m.group(1).strip()
# V4A path headers come from patch CONTENT, not the explicit
# ``path=`` arg — so they're more attacker-influenceable (skill
# content, web extract, prompt injection). Reject ``..`` traversal
# in V4A headers: a legitimate multi-file patch from a single cwd
# can always emit absolute paths or paths relative to the agent's
# cwd without ``..``. The explicit ``path=`` arg is unchanged
# because the agent uses relative ``..`` paths legitimately
# (e.g. ``patch path="../other_module/x.py"`` from a worktree).
if has_traversal_component(v4a_path):
return tool_error(
f"V4A patch header contains '..' traversal: {v4a_path!r}. "
"Use the agent's cwd-relative path (no '..') or an absolute "
"path in '*** Update File:' / '*** Add File:' / '*** Delete File:' headers."
)
_paths_to_check.append(v4a_path)
for _p in _paths_to_check:
sensitive_err = _check_sensitive_path(_p, task_id)
if sensitive_err: