# NousResearch/hermes-agent · Hardening read_file device path blocking bypass

## Open now

- 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.

## TL;DR

- 4 member(s): 0 open issue(s), 2 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #50221. Verify it addresses your case.
- **Recommended:** read PR #50221 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-50221`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.73`: the share of member 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.

## Closed / duplicate issues

- #10141: \[Security\] _is_blocked_device  verify bypass
- #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)

## Prior & competing PRs

- **#50221: fix(file): harden read_file device alias blocking**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `tests/tools/test_file_read_guards.py`, `tools/file_tools.py`
  - fix hunks:
  - `tests/tools/test_file_read_guards.py`

    ```diff
    @@ -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"))
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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
     
    ```
- **#34466: fix(file): harden read_file device alias blocking**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `9078b4bbd`, `#50221`

## Fix status

- Merged to main in PR #50221 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-21`.
- Subsystem: `agent`
