diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 2e545d92c36d..79d743cb570b 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -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. + +## 2024-05-24 - [Sanitize Subprocess Environments in Local Transcription Tools] +**Vulnerability:** Command injection and credential leakage vulnerability in `tools/transcription_tools.py` via `_transcribe_local_command`. +**Learning:** `subprocess.run(command, shell=True)` was used to run configured local STT commands (which can be modified) without sanitizing the environment variables passed to the child process. This means sensitive API keys available in the parent process's environment were being passed down to an untrusted local script or executable. +**Prevention:** Always explicitly pass `env=_sanitize_subprocess_env(os.environ.copy())` to `subprocess.run` or `subprocess.Popen` when `shell=True` is used or when executing external user-defined commands, to prevent secret leakage. diff --git a/tools/process_registry.py b/tools/process_registry.py index fe28b92bdb25..14caccce392b 100644 --- a/tools/process_registry.py +++ b/tools/process_registry.py @@ -41,7 +41,7 @@ import uuid _IS_WINDOWS = platform.system() == "Windows" -from tools.environments.local import _find_shell, _resolve_safe_cwd, _sanitize_subprocess_env +from tools.environments.local import _find_shell, _resolve_safe_cwd from dataclasses import dataclass, field from typing import Any, Dict, List, Optional @@ -506,6 +506,7 @@ def spawn_local( else: from ptyprocess import PtyProcess as _PtyProcessCls user_shell = _find_shell() + from tools.environments.local import _sanitize_subprocess_env pty_env = _sanitize_subprocess_env(os.environ, env_vars) pty_env["PYTHONUNBUFFERED"] = "1" pty_proc = _PtyProcessCls.spawn( @@ -547,6 +548,7 @@ def spawn_local( # Force unbuffered output for Python scripts so progress is visible # during background execution (libraries like tqdm/datasets buffer when # stdout is a pipe, hiding output from process(action="poll")). + from tools.environments.local import _sanitize_subprocess_env bg_env = _sanitize_subprocess_env(os.environ, env_vars) bg_env["PYTHONUNBUFFERED"] = "1" proc = subprocess.Popen( diff --git a/tools/transcription_tools.py b/tools/transcription_tools.py index 663345eb7476..27985aae75f2 100644 --- a/tools/transcription_tools.py +++ b/tools/transcription_tools.py @@ -501,7 +501,19 @@ def _transcribe_local_command(file_path: str, model_name: str) -> Dict[str, Any] language=shlex.quote(language), model=shlex.quote(normalized_model), ) - subprocess.run(command, shell=True, check=True, capture_output=True, text=True) + + # Security: Sanitize the environment to prevent leaking secrets to child processes + from tools.environments.local import _sanitize_subprocess_env + sanitized_env = _sanitize_subprocess_env(os.environ.copy()) + + subprocess.run( + command, + shell=True, + check=True, + capture_output=True, + text=True, + env=sanitized_env + ) txt_files = sorted(Path(output_dir).glob("*.txt")) if not txt_files: