feat(security): sanitize subprocess environment inheritance and scrub credentials

Replace unscrubbed os.environ inheritance with an explicit allowlist
(_SAFE_SUBPROCESS_VARS) containing only variables necessary for bash/python
execution (PATH, locale, terminal, Python virtualenv/site-packages, Windows
essentials) and regex-based credential scrubbing (_SENSITIVE_PATTERN) to
prevent provider tokens, database URLs, and API keys from leaking into child
processes.
This commit is contained in:
Alexandre Teixeira
2026-10-02 18:54:09 +01:00
parent cc14151d10
commit 2a3d0c67d6
2 changed files with 41 additions and 2 deletions
+3 -1
View File
@@ -500,8 +500,10 @@ def _owned_spec(cwd: str, env: Optional[dict], timeout: int, readonly_extra: tup
visible = any(prefix == root or prefix.startswith(root + os.sep) for root in ("/usr", "/etc"))
if not visible and prefix not in _NAMESPACE_RESERVED_DESTS:
readonly.append(prefix)
from src.tool_execution import _agent_subprocess_env
clean_env = _agent_subprocess_env() if env is None else dict(env)
return containment.agent_spec(
cwd, dict(os.environ if env is None else env), timeout,
cwd, clean_env, timeout,
readonly_extra=tuple(dict.fromkeys([*readonly, *readonly_extra])),
)
+38 -1
View File
@@ -1246,8 +1246,45 @@ def _split_bg_marker(content: str):
return False, content
import re as _re
# Variables a legitimate agent bash/python subprocess needs from the host.
# Anything not listed here is never inherited.
_SAFE_SUBPROCESS_VARS = frozenset({
# POSIX execution
"PATH", "LANG", "LC_ALL", "LC_CTYPE", "LC_MESSAGES", "TZ",
"USER", "LOGNAME", "SHELL", "TMPDIR", "TEMP", "TMP",
# Python isolation / virtualenvs
"PYTHONPATH", "PYTHONHOME", "VIRTUAL_ENV",
"ODYSSEUS_PYTHON_TOOL_SITE_PACKAGES",
# Windows system essentials
"SYSTEMROOT", "WINDIR", "COMSPEC", "PATHEXT",
"ALLUSERSPROFILE", "PROGRAMDATA", "COMMONPROGRAMFILES",
"PROGRAMFILES", "PROGRAMFILES(X86)",
# XDG / runtime
"XDG_RUNTIME_DIR", "XDG_DATA_HOME", "XDG_CONFIG_HOME", "XDG_CACHE_HOME",
# Bubblewrap / container paths
"LD_LIBRARY_PATH",
})
# Defence-in-depth: reject any allowlisted variable whose *name* matches
# a credential-bearing pattern (e.g. a user who sets PATH_TOKEN=...).
_SENSITIVE_PATTERN = _re.compile(
r"(?:KEY|TOKEN|SECRET|PASSW|AUTH|CREDENTIAL|PRIVATE|DATABASE_URL)",
_re.IGNORECASE,
)
def _agent_subprocess_env() -> dict:
return {**os.environ, "TERM": "xterm-256color", "COLUMNS": "120", "LINES": "40", "HOME": _AGENT_WORKDIR}
base = {
key: os.environ[key]
for key in _SAFE_SUBPROCESS_VARS
if key in os.environ and not _SENSITIVE_PATTERN.search(key)
}
base.setdefault("PATH", os.environ.get("PATH") or os.defpath or "/usr/local/bin:/usr/bin:/bin")
base.setdefault("LANG", "C.UTF-8")
base.update({"TERM": "xterm-256color", "COLUMNS": "120", "LINES": "40", "HOME": _AGENT_WORKDIR})
return base
async def _direct_fallback(