Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@
**Vulnerability:** The CLI and TUI Gateway executed user-defined `quick_commands` and arbitrary shell commands (`shell.exec`) using `subprocess.run(..., shell=True)` without sanitizing the environment variables passed to the child process.
**Learning:** This exposed sensitive API keys and credentials contained in the main Hermes process environment to these child processes, allowing for easy credential exfiltration by a malicious config or user interaction.
**Prevention:** Always use `tools.environments.local._sanitize_subprocess_env` to filter the environment before passing it to `subprocess` execution mechanisms when executing untrusted or user-supplied shell commands.

## 2025-05-14 - [Sanitize Subprocess Environments in `hermes_cli/tools_config.py`]
**Vulnerability:** `subprocess.run` calls in `hermes_cli/tools_config.py` (specifically in `_pip_install` and `_run_post_setup` hooks) were executing without environment sanitization.
**Learning:** This could leak sensitive Hermes-managed API keys and secrets to external package managers (like `npm` or `pip`) or installation scripts (like the `cua-driver` curl-to-bash script).
**Prevention:** Always apply `_sanitize_subprocess_env` from `tools.environments.local` to the environment dictionary before passing it to `subprocess.run` or `subprocess.Popen`.
19 changes: 15 additions & 4 deletions hermes_cli/tools_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
get_nous_subscription_features,
)
from tools.tool_backend_helpers import fal_key_is_configured, managed_nous_tools_enabled
from tools.environments.local import _sanitize_subprocess_env
from utils import base_url_hostname, is_truthy_value

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -584,7 +585,7 @@ def _pip_install(
(or the last failure for the caller to inspect).
"""
venv_root = Path(sys.executable).parent.parent
uv_env = {**os.environ, "VIRTUAL_ENV": str(venv_root)}
uv_env = _sanitize_subprocess_env(os.environ.copy(), {"VIRTUAL_ENV": str(venv_root)})

uv_bin = shutil.which("uv")
if uv_bin:
Expand All @@ -607,6 +608,7 @@ def _pip_install(
probe = subprocess.run(
pip_cmd + ["--version"],
capture_output=True, text=True, timeout=15,
env=_sanitize_subprocess_env(os.environ.copy()),
)
Comment on lines 608 to 612
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Instead of repeatedly calling _sanitize_subprocess_env(os.environ.copy()) which performs redundant environment copying and filtering, we can reuse the already constructed uv_env dictionary. This also ensures that the VIRTUAL_ENV environment variable is consistently set for all fallback pip commands, matching the behavior of the uv execution path.

Suggested change
probe = subprocess.run(
pip_cmd + ["--version"],
capture_output=True, text=True, timeout=15,
env=_sanitize_subprocess_env(os.environ.copy()),
)
probe = subprocess.run(
pip_cmd + ["--version"],
capture_output=True, text=True, timeout=15,
env=uv_env,
)

if probe.returncode != 0:
raise FileNotFoundError("pip not in venv")
Expand All @@ -615,6 +617,7 @@ def _pip_install(
subprocess.run(
[sys.executable, "-m", "ensurepip", "--upgrade", "--default-pip"],
capture_output=True, text=True, timeout=120, check=True,
env=_sanitize_subprocess_env(os.environ.copy()),
)
Comment on lines 617 to 621
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Reuse the uv_env dictionary here as well to avoid redundant environment sanitization and ensure consistent virtual environment context.

Suggested change
subprocess.run(
[sys.executable, "-m", "ensurepip", "--upgrade", "--default-pip"],
capture_output=True, text=True, timeout=120, check=True,
env=_sanitize_subprocess_env(os.environ.copy()),
)
subprocess.run(
[sys.executable, "-m", "ensurepip", "--upgrade", "--default-pip"],
capture_output=True, text=True, timeout=120, check=True,
env=uv_env,
)

except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
# Synthesize a result so callers see a clean failure path.
Expand All @@ -626,6 +629,7 @@ def _pip_install(
return subprocess.run(
pip_cmd + ["install", *args],
capture_output=capture_output, text=True, timeout=timeout,
env=_sanitize_subprocess_env(os.environ.copy()),
)
Comment on lines 629 to 633
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Reuse the uv_env dictionary here to avoid redundant environment sanitization and ensure consistent virtual environment context.

Suggested change
return subprocess.run(
pip_cmd + ["install", *args],
capture_output=capture_output, text=True, timeout=timeout,
env=_sanitize_subprocess_env(os.environ.copy()),
)
return subprocess.run(
pip_cmd + ["install", *args],
capture_output=capture_output, text=True, timeout=timeout,
env=uv_env,
)



Expand All @@ -646,7 +650,8 @@ def _run_post_setup(post_setup_key: str):
# behaviour as before.
result = subprocess.run(
[npm_bin, "install", "--silent"],
capture_output=True, text=True, cwd=str(PROJECT_ROOT)
capture_output=True, text=True, cwd=str(PROJECT_ROOT),
env=_sanitize_subprocess_env(os.environ.copy()),
)
if result.returncode == 0:
_print_success(" Node.js dependencies installed")
Expand Down Expand Up @@ -722,6 +727,7 @@ def _run_post_setup(post_setup_key: str):
result = subprocess.run(
install_cmd,
capture_output=True, text=True, cwd=str(PROJECT_ROOT), timeout=600,
env=_sanitize_subprocess_env(os.environ.copy()),
)
if result.returncode == 0:
_print_success(" Chromium installed")
Expand Down Expand Up @@ -751,7 +757,8 @@ def _run_post_setup(post_setup_key: str):
# Absolute npm path so .cmd shim executes on Windows.
result = subprocess.run(
[_npm_bin, "install", "--silent"],
capture_output=True, text=True, cwd=str(PROJECT_ROOT)
capture_output=True, text=True, cwd=str(PROJECT_ROOT),
env=_sanitize_subprocess_env(os.environ.copy()),
)
if result.returncode == 0:
_print_success(" Camofox installed")
Expand Down Expand Up @@ -779,6 +786,7 @@ def _run_post_setup(post_setup_key: str):
version = subprocess.run(
["cua-driver", "--version"],
capture_output=True, text=True, timeout=5,
env=_sanitize_subprocess_env(os.environ.copy()),
).stdout.strip()
_print_success(f" cua-driver already installed: {version or 'unknown version'}")
except Exception:
Expand All @@ -798,7 +806,10 @@ def _run_post_setup(post_setup_key: str):
"https://raw.githubusercontent.com/trycua/cua/main/"
"libs/cua-driver/scripts/install.sh)\""
)
result = subprocess.run(install_cmd, shell=True, timeout=300)
result = subprocess.run(
install_cmd, shell=True, timeout=300,
env=_sanitize_subprocess_env(os.environ.copy()),
)
if result.returncode == 0 and shutil.which("cua-driver"):
_print_success(" cua-driver installed.")
_print_info(" IMPORTANT — grant macOS permissions now:")
Expand Down
Loading