Hardening read_file device path blocking bypass

NousResearch/hermes-agent · cluster 10141 · agent

Data as of

Nothing open on GitHub. Fix merged in #50221. Check it covers your case.

The short answer

0open issues
0open PRs
4items grouped
Known?
Yes. 4 related items grouped here, 2 issues already closed.
Fix event
A fix was merged to main in PR #50221. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PR #50221. That is separate from the fix event.
Still open
Nothing open on GitHub.

These issues and pull requests address a security vulnerability where the _is_blocked_device check in the read_file tool could be bypassed using device path aliases or symlinks due to use of expanduser instead of proper path normalization. The work involves tightening the path resolution logic to prevent circumvention of the device blocklist.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #50221 fix(file): harden read_file device alias blocking

    merged to main

    files: tests/tools/test_file_read_guards.py, tools/file_tools.py

    Show the diff
    • tests/tools/test_file_read_guards.py
      @@ -109,6 +109,10 @@ def test_proc_legitimate_files_not_blocked(self):
               for path in ("/proc/cpuinfo", "/proc/meminfo", "/proc/uptime", "/proc/version"):
                   self.assertFalse(_is_blocked_device(path), f"{path} should not be blocked")
       
      +    def test_normpath_alias_to_blocked_device_is_blocked(self):
      +        self.assertTrue(_is_blocked_device("/dev/../dev/zero"))
      +        self.assertTrue(_is_blocked_device("/dev/./urandom"))
      +
           def test_normal_files_not_blocked(self):
               self.assertFalse(_is_blocked_device("/tmp/test.py"))
               self.assertFalse(_is_blocked_device("/home/user/.bashrc"))
      @@ -134,6 +138,17 @@ def test_symlink_to_regular_file_not_blocked(self):
                       self.skipTest(f"symlink unavailable: {exc}")
                   self.assertFalse(_is_blocked_device(link_path))
       
      +    def test_symlink_to_blocked_alias_is_blocked_before_realpath(self):
      +        if not os.path.exists("/dev/stdin"):
      +            self.skipTest("/dev/stdin is not available on this platform")
      +        with tempfile.TemporaryDirectory() as tmpdir:
      +            link_path = os.path.join(tmpdir, "stdin-link")
      +            try:
      +                os.symlink("/dev/../dev/stdin", link_path)
      +            except OSError as exc:
      +                self.skipTest(f"symlink unavailable: {exc}")
      +            self.assertTrue(_is_blocked_device(link_path))
      +
           def test_read_file_tool_rejects_device(self):
               """read_file_tool returns an error without any file I/O."""
               result = json.loads(read_file_tool("/dev/zero", task_id="dev_test"))
    • tools/file_tools.py
      @@ -285,7 +285,7 @@ def _path_resolution_warning(filepath: str, resolved: Path, task_id: str = "defa
       
       def _is_blocked_device_path(path: str) -> bool:
           """Return True for concrete device/fd paths that can hang reads."""
      -    normalized = os.path.expanduser(path)
      +    normalized = os.path.normpath(os.path.expanduser(path))
           if normalized in _BLOCKED_DEVICE_PATHS:
               return True
           # /proc/self/fd/0-2 and /proc/<pid>/fd/0-2 are Linux aliases for stdio
      @@ -306,17 +306,35 @@ def _is_blocked_device(filepath: str) -> bool:
           """Return True if the path would hang the process (infinite output or blocking input).
       
           Check the literal path first so aliases like /dev/stdin are caught before
      -    they resolve to terminal-specific paths. Then check the resolved path so a
      -    workspace symlink to /dev/zero cannot bypass the guard.
      +    they resolve to terminal-specific paths. Then check each symlink hop before
      +    the final resolved path so aliases to devices cannot bypass the guard.
           """
      -    normalized = os.path.expanduser(filepath)
      +    normalized = os.path.normpath(os.path.expanduser(filepath))
           if _is_blocked_device_path(normalized):
               return True
      +
      +    seen: set[str] = set()
      +    current = normalized
      +    for _ in range(20):
      +        try:
      +            target = os.readlink(current)
      +        except OSError:
      +            break
      +        if not os.path.isabs(target):
      +            target = os.path.join(os.path.dirname(current), target)
      +        target = os.path.normpath(target)
      +        if _is_blocked_device_path(target):
      +            return True
      +        if target in seen:
      +            break
      +        seen.add(target)
      +        current = target
      +
           try:
      -        resolved = os.path.realpath(normalized)
      +        resolved = os.path.normpath(os.path.realpath(normalized))
           except (OSError, ValueError):
               return False
      -    if resolved != normalized and _is_blocked_device_path(resolved):
      +    if _is_blocked_device_path(resolved):
               return True
           return False
       

1 PR closed without merging

From the close comments: merged via another change 1

  • #34466 fix(file): harden read_file device alias blocking · merged via another change → 9078b4b, #50221
Closed issues · 2
  • Closed#10141[Security] _is_blocked_device verify bypass
  • Closed#29158[Security tracking] Denial of Service via Device Path Blocklist Bypass in `read_file` Tool (`_is_blocked_device` uses `expanduser` instead of `normpath`) (GHSA-3p9q-rxhm-5x3g)
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.73: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
agent
Merged PRs
#50221
Closed issues
2
Attempts
1 PR closed without merging
Fix link checked
21 Jun 2026

For your agent

The same facts, typed.