diff --git a/tools/code_execution_tool.py b/tools/code_execution_tool.py index 19270c6fe..b2fd52acd 100644 --- a/tools/code_execution_tool.py +++ b/tools/code_execution_tool.py @@ -431,27 +431,57 @@ def execute_code( # Exception: env vars declared by loaded skills (via env_passthrough # registry) or explicitly allowed by the user in config.yaml # (terminal.env_passthrough) are passed through. - _SAFE_ENV_PREFIXES = ("PATH", "HOME", "USER", "LANG", "LC_", "TERM", - "TMPDIR", "TMP", "TEMP", "SHELL", "LOGNAME", - "XDG_", "PYTHONPATH", "VIRTUAL_ENV", "CONDA") - _SECRET_SUBSTRINGS = ("KEY", "TOKEN", "SECRET", "PASSWORD", "CREDENTIAL", - "PASSWD", "AUTH") + # + # SECURITY FIX (V-003): Whitelist-only approach for environment variables. + # Only explicitly allowed environment variables are passed to child. + # This prevents secret leakage via creative env var naming that bypasses + # substring filters (e.g., MY_API_KEY_XYZ instead of API_KEY). + _ALLOWED_ENV_VARS = frozenset([ + # System paths + "PATH", "HOME", "USER", "LOGNAME", "SHELL", + "PWD", "OLDPWD", "CWD", "TMPDIR", "TMP", "TEMP", + # Locale + "LANG", "LC_ALL", "LC_CTYPE", "LC_NUMERIC", "LC_TIME", + "LC_COLLATE", "LC_MONETARY", "LC_MESSAGES", "LC_PAPER", + "LC_NAME", "LC_ADDRESS", "LC_TELEPHONE", "LC_MEASUREMENT", + "LC_IDENTIFICATION", + # Terminal + "TERM", "TERMINFO", "TERMINFO_DIRS", "COLORTERM", + # XDG + "XDG_CONFIG_DIRS", "XDG_CONFIG_HOME", "XDG_CACHE_HOME", + "XDG_DATA_DIRS", "XDG_DATA_HOME", "XDG_RUNTIME_DIR", + "XDG_SESSION_TYPE", "XDG_CURRENT_DESKTOP", + # Python + "PYTHONPATH", "PYTHONHOME", "PYTHONDONTWRITEBYTECODE", + "PYTHONUNBUFFERED", "PYTHONIOENCODING", "PYTHONNOUSERSITE", + "VIRTUAL_ENV", "CONDA_DEFAULT_ENV", "CONDA_PREFIX", + # Hermes-specific (safe only) + "HERMES_RPC_SOCKET", "HERMES_TIMEZONE", + ]) + + # Prefixes that are safe to pass through + _ALLOWED_PREFIXES = ("LC_",) + try: from tools.env_passthrough import is_env_passthrough as _is_passthrough except Exception: _is_passthrough = lambda _: False # noqa: E731 + child_env = {} for k, v in os.environ.items(): # Passthrough vars (skill-declared or user-configured) always pass. if _is_passthrough(k): child_env[k] = v continue - # Block vars with secret-like names. - if any(s in k.upper() for s in _SECRET_SUBSTRINGS): - continue - # Allow vars with known safe prefixes. - if any(k.startswith(p) for p in _SAFE_ENV_PREFIXES): + + # SECURITY: Whitelist-only approach + # Only allow explicitly listed env vars or allowed prefixes + if k in _ALLOWED_ENV_VARS: child_env[k] = v + elif any(k.startswith(p) for p in _ALLOWED_PREFIXES): + child_env[k] = v + # All other env vars are silently dropped + # This prevents secret leakage via creative naming child_env["HERMES_RPC_SOCKET"] = sock_path child_env["PYTHONDONTWRITEBYTECODE"] = "1" # Ensure the hermes-agent root is importable in the sandbox so