Skip to content

Security Fix: Resolve Cross-Platform Path Guard Bypass (OS Path Mismatch) - #4993

Closed
Xowiek wants to merge 1 commit into
NousResearch:mainfrom
Xowiek:fix/cross-platform-path-bypass
Closed

Security Fix: Resolve Cross-Platform Path Guard Bypass (OS Path Mismatch)#4993
Xowiek wants to merge 1 commit into
NousResearch:mainfrom
Xowiek:fix/cross-platform-path-bypass

Conversation

@Xowiek

@Xowiek Xowiek commented Apr 4, 2026

Copy link
Copy Markdown
Contributor

Type: Bug Fix (Security) Components: tools, file_tools Platform: Multi-platform environments (Windows Host → Linux Sandbox)

Overview
This pull request addresses a silent security bypass inside tools/file_tools.py. The _check_sensitive_path guard was implemented to prevent writes to Unix sensitive files such as /etc/shadow and /usr/lib/systemd/ by prefix-matching the input path. However, the path was being parsed by the host's os.path.realpath, leading to an OS path mismatch vulnerability.

When a user runs the Hermes Agent on a Windows machine but executes tasks within a Docker or Linux-based Container Sandbox, sending a malicious path like /etc/shadow would be parsed by Python Windows os.path.realpath as C:\etc\shadow (inheriting the drive letter). The resulting validation resolved.startswith("/etc/") immediately fails, giving a false sense of security while successfully dispatching cat << 'EOF' > /etc/shadow directly to the Linux ShellFileOperations backend.

Details & Root Cause
By using os.path.realpath(...), the tools/file_tools.py bound the safety check entirely to the host OS traversal standards instead of the destination container's filesystem.

Before

try:
resolved = os.path.realpath(os.path.expanduser(filepath))
except (OSError, ValueError):
resolved = filepath

for prefix in _SENSITIVE_PATH_PREFIXES:
if resolved.startswith(prefix):
# ...

    This bypass permitted unchecked programmatic control to container environments where Unix configurations are actively manipulated via write_file_tool and patch_tool.

Remediation
I introduced a dual-resolution safety check. We now employ posixpath.normpath exclusively alongside the conventional host os check. This isolates the traversal safety constraints across boundaries, ensuring that paths evaluated within Linux backends are safely stopped.

After

import posixpath

try:
posix_resolved = posixpath.normpath(filepath)
except Exception:
posix_resolved = filepath

... (host os.path check kept intact for local envs)

for prefix in _SENSITIVE_PATH_PREFIXES:
if resolved.startswith(prefix) or posix_resolved.startswith(prefix):
# Successfully blocked across both environments!

Testing
Verified posixpath.normpath('/etc/shadow') correctly retains /etc/shadow on Windows without prepending C:.
Verified the Agent now fully rejects attempts to deploy malicious modifications into Docker/Containers when hosted by a native Windows system.

@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the detailed write-up and the security focus, @Xowiek!

After reviewing current main, this protection is already in place via a different — but equivalent — implementation. This is an automated hermes-sweeper review.

What exists on main (tools/file_tools.py, lines 159–175):

  • _check_sensitive_path already performs a dual check: it evaluates both resolved (task-CWD-aware Path.resolve()) and normalized (os.path.normpath(os.path.expanduser(filepath))) against every sensitive prefix.
  • The normalized branch retains /etc/shadow as /etc/shadow regardless of host OS path resolution, which is the same protection your posixpath.normpath provides on Linux (the only platform where Docker/Linux containers run).
  • The os.path.realpath call your PR references was already removed from this function in a prior refactor; it no longer exists in the file.

Evidence:

# tools/file_tools.py ~line 165 (current main)
normalized = os.path.normpath(os.path.expanduser(filepath))
...
for prefix in _SENSITIVE_PATH_PREFIXES:
    if resolved.startswith(prefix) or normalized.startswith(prefix):
        return _err

The gap you identified was real and worth fixing — it looks like it was addressed independently before this PR landed. Closing as implemented on main.

@teknium1 teknium1 closed this Apr 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants