fix(security): isolate tool subprocesses with PID namespaces#4432
fix(security): isolate tool subprocesses with PID namespaces#4432raktes wants to merge 4 commits into
Conversation
The env blocklist in _sanitize_subprocess_env() and the _SECRET_SUBSTRINGS filter in code_execution_tool can be bypassed on Linux: any same-UID child process can read /proc/<parent_pid>/environ and recover every stripped variable. Wrap tool subprocesses in a PID namespace via `unshare --user --pid --fork --mount-proc`. Inside the namespace, /proc is remounted so the parent's PID does not exist — the child cannot address or scan for the parent's environ. Falls back to the current (unwrapped) behavior on non-Linux platforms and when unshare is unavailable (e.g. restricted containers). Fixes NousResearch#4427
|
@raktes @teknium1 this is a good change. I think with this change, if the user runs all of hermes itself in docker and secrets are only passed through environment variables, then there is no obvious way to use terminal or execute_code to exfiltrate secrets. Any other config is still vulnerable because the child process still has access to the agent's file system. It can't use the exact vector I demonstrated, but another vector that still works is a trivial exercise I will leave to the reader. I would like to see that acknowledged in the security notes. |
Remove PID namespace wrapping from persistent local shells — their timeout/interrupt cleanup depends on host-visible PIDs for child reaping. Keep isolation on execute_code and short-lived foreground local terminal commands only. Replace the shutil.which check with a real unshare probe so systems where user namespaces are disabled fall back cleanly instead of failing hard. Add limitation warning to security docs: filesystem secrets and persistent/background/PTY shells remain unprotected.
|
@jeremyjh 👍 Added an explicit limitation warning in the security docs covering the filesystem gap. Also had to narrow down the scope: PID namespace isolation now only applies to |
The read_file tool and terminal cat can access /proc/self/environ to recover all process env vars including secrets stripped by the subprocess blocklist. Output redaction partially mitigates (catches known-format tokens) but misses custom/proprietary key formats, especially when values are printed without their key names. Add /proc/*/environ, /proc/*/cmdline, and /proc/*/maps to the blocked device paths in _is_blocked_device(): - /proc/*/environ: leaks full process env (API keys, tokens) - /proc/*/cmdline: leaks command-line args (may contain passwords) - /proc/*/maps: leaks memory layout (ASLR bypass for exploitation) Legitimate /proc reads (cpuinfo, meminfo, uptime, version) remain accessible — the check only blocks per-pid pseudo-files with known sensitive suffixes. Complements PR NousResearch#4432 (PID namespace isolation for child processes) which prevents children from reading the parent's /proc, but does not prevent the parent process itself from being read via file tools. Partially addresses NousResearch#4427 Changes: tools/file_tools.py | +6 tests/tools/test_file_read_guards.py | +18 -1
|
Verified the # Parent has FAKE_API_KEY in its exec-time env
# Child spawned with stripped env reads /proc/<ppid>/environ
>>> output: sk-super-secret-key-12345 # leakedHowever, the same child can also just do this: open(os.path.expanduser('~/.hermes/.env')).read()
# OPENROUTER_API_KEY=sk-or-...
# FIRECRAWL_API_KEY=...
# NOUS_API_KEY=...The filesystem path is wider, easier, and unaffected by this fix. The PR itself acknowledges this limitation. Adding The env var stripping in Appreciate the thorough security analysis and the clean implementation — the PR itself is well done. But we'd rather not add this complexity for a partial fix. If we revisit local backend hardening in the future, it would need to address filesystem access too (likely via mount namespaces or seccomp), and at that point we'd be reimplementing what Docker already does. |
|
@teknium1 execute_code doesn't use docker, but it calls itself a sandbox. The real issue is that even if you run all of hermes in docker, there are still secrets in the environment variables. So execute_code will always have access to them, without a fix like this one. There is no security barrier. |
The read_file tool and terminal cat can access /proc/self/environ to recover all process env vars including secrets stripped by the subprocess blocklist. Output redaction partially mitigates (catches known-format tokens) but misses custom/proprietary key formats, especially when values are printed without their key names. Add /proc/*/environ, /proc/*/cmdline, and /proc/*/maps to the blocked device paths in _is_blocked_device(): - /proc/*/environ: leaks full process env (API keys, tokens) - /proc/*/cmdline: leaks command-line args (may contain passwords) - /proc/*/maps: leaks memory layout (ASLR bypass for exploitation) Legitimate /proc reads (cpuinfo, meminfo, uptime, version) remain accessible — the check only blocks per-pid pseudo-files with known sensitive suffixes. Complements PR NousResearch#4432 (PID namespace isolation for child processes) which prevents children from reading the parent's /proc, but does not prevent the parent process itself from being read via file tools. Partially addresses NousResearch#4427 Changes: tools/file_tools.py | +6 tests/tools/test_file_read_guards.py | +18 -1
…4609) The read_file tool and terminal cat can access /proc/self/environ to recover all process env vars including secrets stripped by the subprocess blocklist. Output redaction partially mitigates (catches known-format tokens) but misses custom/proprietary key formats, especially when values are printed without their key names. Add /proc/*/environ, /proc/*/cmdline, and /proc/*/maps to the blocked device paths in _is_blocked_device(): - /proc/*/environ: leaks full process env (API keys, tokens) - /proc/*/cmdline: leaks command-line args (may contain passwords) - /proc/*/maps: leaks memory layout (ASLR bypass for exploitation) Legitimate /proc reads (cpuinfo, meminfo, uptime, version) remain accessible — the check only blocks per-pid pseudo-files with known sensitive suffixes. Complements PR #4432 (PID namespace isolation for child processes) which prevents children from reading the parent's /proc, but does not prevent the parent process itself from being read via file tools. Partially addresses #4427 Changes: tools/file_tools.py | +6 tests/tools/test_file_read_guards.py | +18 -1 Co-authored-by: dsr-restyn <dsr-restyn@users.noreply.github.com>
…ousResearch#4609) The read_file tool and terminal cat can access /proc/self/environ to recover all process env vars including secrets stripped by the subprocess blocklist. Output redaction partially mitigates (catches known-format tokens) but misses custom/proprietary key formats, especially when values are printed without their key names. Add /proc/*/environ, /proc/*/cmdline, and /proc/*/maps to the blocked device paths in _is_blocked_device(): - /proc/*/environ: leaks full process env (API keys, tokens) - /proc/*/cmdline: leaks command-line args (may contain passwords) - /proc/*/maps: leaks memory layout (ASLR bypass for exploitation) Legitimate /proc reads (cpuinfo, meminfo, uptime, version) remain accessible — the check only blocks per-pid pseudo-files with known sensitive suffixes. Complements PR NousResearch#4432 (PID namespace isolation for child processes) which prevents children from reading the parent's /proc, but does not prevent the parent process itself from being read via file tools. Partially addresses NousResearch#4427 Changes: tools/file_tools.py | +6 tests/tools/test_file_read_guards.py | +18 -1 Co-authored-by: dsr-restyn <dsr-restyn@users.noreply.github.com>
…ousResearch#4609) The read_file tool and terminal cat can access /proc/self/environ to recover all process env vars including secrets stripped by the subprocess blocklist. Output redaction partially mitigates (catches known-format tokens) but misses custom/proprietary key formats, especially when values are printed without their key names. Add /proc/*/environ, /proc/*/cmdline, and /proc/*/maps to the blocked device paths in _is_blocked_device(): - /proc/*/environ: leaks full process env (API keys, tokens) - /proc/*/cmdline: leaks command-line args (may contain passwords) - /proc/*/maps: leaks memory layout (ASLR bypass for exploitation) Legitimate /proc reads (cpuinfo, meminfo, uptime, version) remain accessible — the check only blocks per-pid pseudo-files with known sensitive suffixes. Complements PR NousResearch#4432 (PID namespace isolation for child processes) which prevents children from reading the parent's /proc, but does not prevent the parent process itself from being read via file tools. Partially addresses NousResearch#4427 Changes: tools/file_tools.py | +6 tests/tools/test_file_read_guards.py | +18 -1 Co-authored-by: dsr-restyn <dsr-restyn@users.noreply.github.com>
…ousResearch#4609) The read_file tool and terminal cat can access /proc/self/environ to recover all process env vars including secrets stripped by the subprocess blocklist. Output redaction partially mitigates (catches known-format tokens) but misses custom/proprietary key formats, especially when values are printed without their key names. Add /proc/*/environ, /proc/*/cmdline, and /proc/*/maps to the blocked device paths in _is_blocked_device(): - /proc/*/environ: leaks full process env (API keys, tokens) - /proc/*/cmdline: leaks command-line args (may contain passwords) - /proc/*/maps: leaks memory layout (ASLR bypass for exploitation) Legitimate /proc reads (cpuinfo, meminfo, uptime, version) remain accessible — the check only blocks per-pid pseudo-files with known sensitive suffixes. Complements PR NousResearch#4432 (PID namespace isolation for child processes) which prevents children from reading the parent's /proc, but does not prevent the parent process itself from being read via file tools. Partially addresses NousResearch#4427 Changes: tools/file_tools.py | +6 tests/tools/test_file_read_guards.py | +18 -1 Co-authored-by: dsr-restyn <dsr-restyn@users.noreply.github.com> #AI commit#
…ousResearch#4609) The read_file tool and terminal cat can access /proc/self/environ to recover all process env vars including secrets stripped by the subprocess blocklist. Output redaction partially mitigates (catches known-format tokens) but misses custom/proprietary key formats, especially when values are printed without their key names. Add /proc/*/environ, /proc/*/cmdline, and /proc/*/maps to the blocked device paths in _is_blocked_device(): - /proc/*/environ: leaks full process env (API keys, tokens) - /proc/*/cmdline: leaks command-line args (may contain passwords) - /proc/*/maps: leaks memory layout (ASLR bypass for exploitation) Legitimate /proc reads (cpuinfo, meminfo, uptime, version) remain accessible — the check only blocks per-pid pseudo-files with known sensitive suffixes. Complements PR NousResearch#4432 (PID namespace isolation for child processes) which prevents children from reading the parent's /proc, but does not prevent the parent process itself from being read via file tools. Partially addresses NousResearch#4427 Changes: tools/file_tools.py | +6 tests/tools/test_file_read_guards.py | +18 -1 Co-authored-by: dsr-restyn <dsr-restyn@users.noreply.github.com>
…ousResearch#4609) The read_file tool and terminal cat can access /proc/self/environ to recover all process env vars including secrets stripped by the subprocess blocklist. Output redaction partially mitigates (catches known-format tokens) but misses custom/proprietary key formats, especially when values are printed without their key names. Add /proc/*/environ, /proc/*/cmdline, and /proc/*/maps to the blocked device paths in _is_blocked_device(): - /proc/*/environ: leaks full process env (API keys, tokens) - /proc/*/cmdline: leaks command-line args (may contain passwords) - /proc/*/maps: leaks memory layout (ASLR bypass for exploitation) Legitimate /proc reads (cpuinfo, meminfo, uptime, version) remain accessible — the check only blocks per-pid pseudo-files with known sensitive suffixes. Complements PR NousResearch#4432 (PID namespace isolation for child processes) which prevents children from reading the parent's /proc, but does not prevent the parent process itself from being read via file tools. Partially addresses NousResearch#4427 Changes: tools/file_tools.py | +6 tests/tools/test_file_read_guards.py | +18 -1 Co-authored-by: dsr-restyn <dsr-restyn@users.noreply.github.com>
What does this PR do?
The env blocklist in
_sanitize_subprocess_env()and the_SECRET_SUBSTRINGSfilter incode_execution_toolcan be bypassed on Linux: any same-UID child process can read/proc/<parent_pid>/environand recover every stripped variable.This PR wraps short-lived tool subprocesses in a PID namespace via
unshare --user --pid --fork --mount-proc. Inside the namespace,/procis remounted so the parent's PID does not exist — the child cannot address or scan for the parent's environ.Falls back to the current (unwrapped) behavior on non-Linux platforms and when
unshareis unavailable (e.g. restricted containers).Related Issue
Partially addresses #4427
Scope
PID namespace isolation is applied to:
execute_code— sandboxed Python script spawnterminalcommands — one-shotsubprocess.PopenpathNot applied to persistent local shells,
terminal(background=true), orterminal(pty=true), because their timeout/interrupt cleanup depends on host-visible PIDs ($$pluspkill -P). Inside a PID namespace that PID becomes namespace-local, which makes child reaping unreliable.Known limitations
~/.hermes/.envand~/.hermes/auth.jsondirectly. Running Hermes in a container with secrets provided exclusively through environment variables provides the strongest isolation.Type of Change
Changes Made
tools/environments/local.py: Added_have_unshare()runtime probe and_pidns_wrap()helper that prefixes commands withunshare --user --pid --fork --mount-procon Linux. Applied to the one-shot foreground command spawn path.tools/code_execution_tool.py: Imports_pidns_wrapfromlocal.pyand applies it to the sandboxed Python script spawn.tests/tools/test_local_env_blocklist.py: Added unit tests for_pidns_wrap(wrap/no-op behavior),_have_unshareprobe, persistent-shell-not-wrapped assertion, and a regression integration test that spawns a real child in a PID namespace and verifies it cannot recover stripped env vars via/proc.website/docs/user-guide/security.md: Added limitation warning documenting filesystem secret exposure and persistent-shell gap.How to Test
pytest tests/tools/test_local_env_blocklist.py::TestPidNamespaceWrap -vunshareavailable):pytest tests/tools/test_local_env_blocklist.py::TestProcEnvironIsolation -vunshareis unavailableChecklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -q— some pre-existing test failures unrelated to this PR (same failures reproduced onorigin/main)Documentation & Housekeeping
cli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/A