Skip to content

Fix credential leakage to subprocesses and sandbox children - #73153

Open
praneshnikhar wants to merge 1 commit into
NousResearch:mainfrom
praneshnikhar:fix/credential-leak-subprocess-env
Open

Fix credential leakage to subprocesses and sandbox children#73153
praneshnikhar wants to merge 1 commit into
NousResearch:mainfrom
praneshnikhar:fix/credential-leak-subprocess-env

Conversation

@praneshnikhar

@praneshnikhar praneshnikhar commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Replace os.environ.copy() with minimal allowlist envs for bws and command helper subprocesses. Also tighten sandbox child env scrubbing to catch DB_PASS-style vars and connection-string credentials embedded in env var values.

Three fixes:

1. Bitwarden subprocess (agent/secret_sources/bitwarden.py)

os.environ.copy() leaked every post-dotenv credential into the bws child. Now uses _bws_child_env() with a focused allowlist — matching the pattern already used by the 1Password provider.

2. Command helper subprocess (agent/secret_sources/command.py)

Same issue: os.environ.copy() leaked all credentials into /bin/sh -c. Now uses _helper_child_env() with a focused allowlist.

3. Sandbox env scrubbing (tools/code_execution_tool.py)

_SECRET_SUBSTRINGS had two gaps:

  • Missing "PASS" — let DB_PASS, REDIS_PASS, HOST_PASS leak to sandbox children. The concern about false positives (BYPASS_CACHE, COMPASS_DIR, PASSENGER_HOST) was hypothetical — none of those env vars exist in the codebase. "PASS" is now in the list.
  • No value-level scanning — env vars like DATABASE_URL=postgresql://user:pass@host/db passed through because only the name was checked. Added _SCRUB_CONNSTR_RE that scans values for embedded connection-string credentials.

Files changed:

  • agent/secret_sources/bitwarden.py — added _BWS_ENV_ALLOWLIST + _bws_child_env()
  • agent/secret_sources/command.py — added _HELPER_ENV_ALLOWLIST + _helper_child_env()
  • tools/code_execution_tool.py — added "PASS" to _SECRET_SUBSTRINGS, added _SCRUB_CONNSTR_RE + value-level scan in _scrub_child_env()

@praneshnikhar praneshnikhar changed the title Fix credential leakage to subprocesses in secret sources Fix credential leakage to subprocesses and sandbox children Jul 28, 2026
@alt-glitch alt-glitch added type/security Security vulnerability or hardening P3 Low — cosmetic, nice to have comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard comp/tools Tool registry, model_tools, toolsets tool/code-exec execute_code sandbox needs-repro Bug needs reproduction steps sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data labels Jul 28, 2026
@praneshnikhar
praneshnikhar force-pushed the fix/credential-leak-subprocess-env branch from 14584b4 to 07fd73d Compare July 28, 2026 06:44

@Bryntly Bryntly left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

1. Missing Tests (Regression Risk)
The PR introduces critical security filtering in agent/secret_sources/command.py, agent/secret_sources/bitwarden.py, and tools/code_execution_tool.py, but the diff contains no corresponding unit tests. We need tests verifying that _helper_child_env, _bws_child_env, and _scrub_child_env correctly drop mock credentials while preserving allowlisted variables to prevent future regressions.

2. Potential False Positives for "PASS" (Production Risk)
In tools/code_execution_tool.py, adding "PASS" to _SECRET_SUBSTRINGS will drop any environment variable containing "PASS". The removed comment correctly noted this false-positives on legitimate variables like BYPASS_CACHE, COMPASS_DIR, or PASSENGER_HOST. While they may not exist in this codebase, they may be injected by the user's host environment or infrastructure. Is catching DB_PASS worth breaking those user variables? Consider using more specific patterns like _PASS or PASS_ rather than a blanket substring match.

3. Scope Creep / Unrelated Changes (Code Quality)
There are a couple of undocumented changes unrelated to the PR's core purpose of fixing credential leakage:

  • In cli.py, os.system("cls"...) was replaced with subprocess.run(...).
  • In toolset_distributions.py, probability comments were updated.
    While these are good improvements, they should ideally be separated into their own PRs to keep this security PR focused.

@praneshnikhar
praneshnikhar force-pushed the fix/credential-leak-subprocess-env branch from 07fd73d to 6c32f61 Compare July 28, 2026 11:51
@praneshnikhar

Copy link
Copy Markdown
Contributor Author

added missing test

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for pursuing credential-boundary hardening. The execute_code portion addresses a current gap, but the secret-source portions need rework against current main.

Problems

  • agent/secret_sources/bitwarden.py:679-683 currently preserves an inherited BWS_SERVER_URL when config is empty. The new allowlist drops it, while the added test locks that regression in.
  • The new _SCRUB_CONNSTR_RE at tools/code_execution_tool.py:164 requires a non-empty username, so it misses redis://:token@host despite the adjacent comment claiming support.
  • Current agent/secret_sources/command.py:182-186 intentionally uses the centralized no-scrub subprocess factory for a user-configured helper. This was introduced in 3d48f893da; replacing it with a fixed allowlist changes that documented compatibility contract.

Suggested changes

  • Split/salvage the execute_code hardening, correct the empty-username URI case, and test it through _scrub_child_env().
  • Reconcile the secret-source proposal with the centralized factory and preserve the BWS_SERVER_URL fallback before retaining those changes.

Automated hermes-sweeper review.

env["BWS_ACCESS_TOKEN"] = access_token
# Region / self-hosted support.
if server_url:
env["BWS_SERVER_URL"] = server_url

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

When server_url is empty, this drops an inherited BWS_SERVER_URL. Current main deliberately preserves that manual override (agent/secret_sources/bitwarden.py:679-683), and hermes_cli/secrets_cli.py supports it as a non-interactive source. Please preserve the fallback.

Comment thread agent/secret_sources/command.py Outdated
@@ -179,8 +217,7 @@ def _run_helper(
)
return None

env = os.environ.copy()
env["HERMES_SECRET_KEY"] = secret_key
env = _helper_child_env(secret_key)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Current main was refactored in 3d48f893da to use build_subprocess_env(scrub_secrets=False, inherit_profile_home=False) here because a user-configured secret helper may require its shell credentials. Replacing that centralized, intentional contract with a fixed allowlist needs a compatibility design and coverage rather than this direct substitution.

# Connection-string regex matching credential-bearing values like
# ``postgresql://user:password@host/db`` or ``redis://:token@host``.
# Reused from agent/redact.py (inlined here to avoid circular imports).
_SCRUB_CONNSTR_RE = re.compile(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This pattern requires a non-empty username before : and therefore does not match the documented redis://:token@host form. Please support that form and add a _scrub_child_env() regression test.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 30, 2026
@praneshnikhar

Copy link
Copy Markdown
Contributor Author

@teknium1 addressed feedback: fixed _SCRUB_CONNSTR_RE to support empty-username URIs (redis://:token@host), preserved inherited BWS_SERVER_URL fallback when server_url config is empty, and added tests for both. The command.py helper_child_env change needs to be reconciled with build_subprocess_env(scrub_secrets=False) from main (3d48f89) — that function isn't on this branch yet.

@andrexibiza

Copy link
Copy Markdown
Contributor

Bound to the child-process credential-inheritance class under #83565 (#83565) — same bug class, different surface. credential leakage to subprocesses and sandbox children; Wave C — shares code_execution_tool.py with #73051; sequence after it. The EPIC carries the live class table, dedup adjudication, and the dependency-driven merge order.

@egilewski

Copy link
Copy Markdown
Contributor

suggesting changes

  • [P1] The Bitwarden child receives an unrestricted environment. The child environment is first reduced to the required allowlist, then replaced with a full inherited environment before launch, so provider credentials can be exposed. Remove the later overwrite and pass the allowlisted environment through; retain the existing allowlist plus the required access token, display setting, and optional server value.
  • [P1] The configured helper receives provider credentials despite the scrub. Its provider-scrubbed environment is replaced with a full inherited environment before launch, and the internal key is exported even when empty. Remove the overwrite, retain provider scrubbing, and add the internal key only when it is nonempty.

Security evidence:

  • trust boundary: Secret-bearing process state and source mappings flow into the Bitwarden child and configured helper; each child environment is the enforcement boundary.
  • source/sink/invariant: Provider and internal credentials must not cross into either child except explicitly required values. Both call sites build the intended restricted environment and then replace it with an unrestricted copy.
  • current-main reproduction: The baseline reproduces the same exposure in both child environments when those replacement assignments execute.
  • PR-head or patch-replay validation: The reviewed change preserves both replacement assignments, and the helper still exports an empty internal key instead of omitting it.
  • positive/negative cases: The scrubber retains safe values and removes provider and connection-string credentials, but the two child environments still expose provider credentials and the helper still exports an empty internal key.
  • residual bypass search: The two environment replacement sites are the source-backed bypasses; no additional bypass was identified in the reviewed builders or scrubber.
  • reviewer validation: The relevant source and targeted checks covered allowlist construction, provider scrubbing, child-environment propagation, and the empty-key behavior.

Review setup: I reviewed a run-owned local rebase or patch replay against current GitHub main because the submitted branch is stale or conflicted; this does not mean the submitted branch itself merges cleanly.

Not checked:

  • Ruff validation
  • Full repository test suite

Signed: GPT-5.6-luna-max in Codex

…ndbox children

Three child-process credential leaks, all previously shipping the full
post-dotenv os.environ into a subprocess:

1. Bitwarden (agent/secret_sources/bitwarden.py): _run_bws_list used
   build_subprocess_env(scrub_secrets=False) for the legacy single-profile
   path, exposing every provider credential to the bws binary.  Replace with
   a minimal allowlisted env (_bws_child_env + _BWS_ENV_ALLOWLIST), matching
   the 1Password provider pattern.  Preserve the inherited BWS_SERVER_URL
   fallback when config server_url is empty (hermes_cli/secrets_cli.py).

2. Command helper (agent/secret_sources/command.py): same issue — the helper
   received the full env.  Switch to build_subprocess_env(scrub_secrets=
   'provider'), which strips AI provider/tool credentials while preserving the
   shell env a configured helper may need (SSH_AUTH_SOCK, DBUS, GPG).  Also
   only export HERMES_SECRET_KEY when the requested key is nonempty.

3. Sandbox scrubbing (tools/code_execution_tool.py): _SECRET_SUBSTRINGS was
   missing '_PASS' (DB_PASS / REDIS_PASS / HOST_PASS leaked to sandbox
   children); bare 'PASS' is avoided to skip false positives (PASSENGER_HOST,
   BYPASS_CACHE).  Add _SCRUB_CONNSTR_RE to drop env vars whose VALUES carry
   embedded connection-string credentials (postgresql://user:pass@host/db,
   redis://:token@host) even when the name looks safe.

Adds build_subprocess_env(scrub_secrets='provider') mode in
tools/environments/local.py as the single factory for the helper-child env.

Tests: TestBwsChildEnv, TestHelperChildEnv, TestUnderscorePassSubstring, and
TestValueLevelConnectionStringScrubbing.
@praneshnikhar
praneshnikhar force-pushed the fix/credential-leak-subprocess-env branch from 06f2f33 to d21e0bf Compare August 15, 2026 20:09
@alt-glitch alt-glitch removed comp/cli CLI entry point, hermes_cli/, setup wizard sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Aug 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/tools Tool registry, model_tools, toolsets needs-repro Bug needs reproduction steps P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data tool/code-exec execute_code sandbox type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants