mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-10 08:32:09 +00:00
fix(security): narrow Bedrock subprocess strip to inference bearer token only
Scopes the AWS_SDK subprocess strip down from the full AWS credential chain to just AWS_BEARER_TOKEN_BEDROCK — the only Hermes-managed *inference* secret (analogous to OPENAI_API_KEY). The general AWS credential chain (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN / AWS_PROFILE / config + role pointers) is intentionally left inheritable. Why: per SECURITY.md §3.2 the local terminal is the user's trusted operator shell. Hard-blocklisting the general chain would (a) regress *every* user who runs aws/terraform/cdk/boto3 in the agent terminal — not just Bedrock users, since PROVIDER_REGISTRY is iterated unconditionally at import — and (b) be unrecoverable, because env_passthrough.py refuses to re-allow anything in _HERMES_PROVIDER_ENV_BLOCKLIST (GHSA-rhgp-j443-p4rf). The narrow strip closes the reported leak (opencode enumerating the Bedrock catalog off the leaked bearer token) with no capability loss. Keeps zapabob's self-healing auth_type=="aws_sdk" mechanism so any future SDK-cred provider is covered automatically. Tests: bearer token stripped + general chain preserved (no-regression guard), on both the runtime strip path and the blocklist-membership path. Co-authored-by: zapabob <1920071390@campus.ouj.ac.jp>
This commit is contained in:
parent
95b5b72404
commit
6bebab4761
2 changed files with 80 additions and 37 deletions
|
|
@ -93,25 +93,58 @@ class TestProviderEnvBlocklist:
|
|||
for var in registry_vars:
|
||||
assert var not in result_env, f"{var} leaked into subprocess env"
|
||||
|
||||
def test_aws_sdk_provider_vars_are_stripped(self):
|
||||
"""AWS SDK credential-chain vars must not leak to subprocesses."""
|
||||
aws_vars = {
|
||||
def test_bedrock_bearer_token_is_stripped(self):
|
||||
"""The Bedrock-specific bearer token is a Hermes inference secret
|
||||
(analogous to OPENAI_API_KEY) and must not leak into subprocesses.
|
||||
|
||||
Regression for #32314: AWS_BEARER_TOKEN_BEDROCK leaked into terminal /
|
||||
execute_code children because the ``bedrock`` ProviderConfig declares
|
||||
``api_key_env_vars=()`` (auth_type="aws_sdk") and the blocklist builder
|
||||
only consulted that field. The reporter caught it when ``opencode
|
||||
models`` run inside a Hermes terminal enumerated the entire Bedrock
|
||||
catalog off the leaked bearer token.
|
||||
"""
|
||||
result_env = _run_with_env(extra_os_env={
|
||||
"AWS_BEARER_TOKEN_BEDROCK": "bedrock-bearer-secret",
|
||||
})
|
||||
|
||||
assert "AWS_BEARER_TOKEN_BEDROCK" not in result_env, (
|
||||
"AWS_BEARER_TOKEN_BEDROCK leaked into subprocess env (see #32314)"
|
||||
)
|
||||
|
||||
def test_general_aws_credential_chain_is_preserved(self):
|
||||
"""The GENERAL AWS credential chain must STILL pass through to
|
||||
subprocesses — this is the no-regression guard for #32314.
|
||||
|
||||
Per SECURITY.md §3.2 the local terminal is the user's trusted operator
|
||||
shell. A user running ``aws``/``terraform``/``cdk``/``boto3`` in the
|
||||
agent terminal must keep the same AWS access their own shell has.
|
||||
Stripping these would (a) break every user who does AWS work in the
|
||||
agent terminal — not just Bedrock users, since the registry is iterated
|
||||
unconditionally — and (b) be unrecoverable, because env_passthrough.py
|
||||
refuses to re-allow anything in _HERMES_PROVIDER_ENV_BLOCKLIST
|
||||
(GHSA-rhgp-j443-p4rf). Only the Bedrock inference bearer token is
|
||||
Hermes-managed; the rest belongs to the user.
|
||||
"""
|
||||
general_chain = {
|
||||
"AWS_ACCESS_KEY_ID": "AKIAIOSFODNN7EXAMPLE",
|
||||
"AWS_SECRET_ACCESS_KEY": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
||||
"AWS_SESSION_TOKEN": "session-token",
|
||||
"AWS_PROFILE": "production",
|
||||
"AWS_DEFAULT_REGION": "us-east-1",
|
||||
"AWS_REGION": "us-east-1",
|
||||
"AWS_SHARED_CREDENTIALS_FILE": "/home/user/.aws/credentials",
|
||||
"AWS_CONFIG_FILE": "/home/user/.aws/config",
|
||||
"AWS_WEB_IDENTITY_TOKEN_FILE": "/var/run/secrets/token",
|
||||
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI": "/v2/credentials/123",
|
||||
"AWS_CONTAINER_CREDENTIALS_FULL_URI": "http://169.254.170.2/v2/credentials/123",
|
||||
"AWS_CONTAINER_AUTHORIZATION_TOKEN": "container-token",
|
||||
"AWS_BEARER_TOKEN_BEDROCK": "bedrock-bearer",
|
||||
"AWS_ROLE_ARN": "arn:aws:iam::123456789012:role/example",
|
||||
}
|
||||
result_env = _run_with_env(extra_os_env=aws_vars)
|
||||
result_env = _run_with_env(extra_os_env=general_chain)
|
||||
|
||||
for var in aws_vars:
|
||||
assert var not in result_env, f"{var} leaked into subprocess env"
|
||||
for var, value in general_chain.items():
|
||||
assert result_env.get(var) == value, (
|
||||
f"{var} was stripped from subprocess env — this is a "
|
||||
f"capability regression (see #32314 discussion)"
|
||||
)
|
||||
|
||||
def test_non_registry_provider_vars_are_stripped(self):
|
||||
"""Extra provider vars not in PROVIDER_REGISTRY must also be blocked."""
|
||||
|
|
@ -233,27 +266,35 @@ class TestBlocklistCoverage:
|
|||
f"(provider={pconfig.id}) missing from blocklist"
|
||||
)
|
||||
|
||||
def test_aws_sdk_provider_vars_are_in_blocklist(self):
|
||||
"""auth_type='aws_sdk' providers rely on credential-chain vars, not API keys."""
|
||||
aws_vars = {
|
||||
def test_bedrock_bearer_token_is_in_blocklist(self):
|
||||
"""auth_type='aws_sdk' providers contribute their Hermes-managed
|
||||
inference token (the Bedrock bearer) to the blocklist, keyed off
|
||||
auth_type so any future SDK-cred provider is covered automatically."""
|
||||
assert "AWS_BEARER_TOKEN_BEDROCK" in _HERMES_PROVIDER_ENV_BLOCKLIST
|
||||
|
||||
def test_general_aws_chain_not_in_blocklist(self):
|
||||
"""The general AWS credential chain must NOT be in the blocklist —
|
||||
no-regression guard for #32314. These belong to the user's trusted
|
||||
operator shell (SECURITY.md §3.2), not to Hermes, and blocklisting
|
||||
them would be unrecoverable via env_passthrough (GHSA-rhgp-j443-p4rf).
|
||||
"""
|
||||
general_chain = {
|
||||
"AWS_ACCESS_KEY_ID",
|
||||
"AWS_SECRET_ACCESS_KEY",
|
||||
"AWS_SESSION_TOKEN",
|
||||
"AWS_SECURITY_TOKEN",
|
||||
"AWS_PROFILE",
|
||||
"AWS_DEFAULT_PROFILE",
|
||||
"AWS_DEFAULT_REGION",
|
||||
"AWS_REGION",
|
||||
"AWS_SHARED_CREDENTIALS_FILE",
|
||||
"AWS_CONFIG_FILE",
|
||||
"AWS_WEB_IDENTITY_TOKEN_FILE",
|
||||
"AWS_ROLE_ARN",
|
||||
"AWS_ROLE_SESSION_NAME",
|
||||
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
|
||||
"AWS_CONTAINER_CREDENTIALS_FULL_URI",
|
||||
"AWS_CONTAINER_AUTHORIZATION_TOKEN",
|
||||
"AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE",
|
||||
"AWS_BEARER_TOKEN_BEDROCK",
|
||||
}
|
||||
assert aws_vars.issubset(_HERMES_PROVIDER_ENV_BLOCKLIST)
|
||||
leaked_block = general_chain & _HERMES_PROVIDER_ENV_BLOCKLIST
|
||||
assert not leaked_block, (
|
||||
f"General AWS chain vars must stay inheritable, but these are "
|
||||
f"blocklisted: {sorted(leaked_block)} (capability regression, #32314)"
|
||||
)
|
||||
|
||||
def test_extra_auth_vars_covered(self):
|
||||
"""Non-registry auth vars (ANTHROPIC_TOKEN, CLAUDE_CODE_OAUTH_TOKEN)
|
||||
|
|
|
|||
|
|
@ -75,22 +75,24 @@ def _resolve_safe_cwd(cwd: str) -> str:
|
|||
# Hermes-internal env vars that should NOT leak into terminal subprocesses.
|
||||
_HERMES_PROVIDER_ENV_FORCE_PREFIX = "_HERMES_FORCE_"
|
||||
|
||||
# Hermes-managed AWS *inference* credentials for ``auth_type="aws_sdk"``
|
||||
# providers (Bedrock). Scoped DELIBERATELY NARROW: this lists only the
|
||||
# Bedrock-specific bearer token, which is a Hermes inference secret exactly
|
||||
# analogous to ``OPENAI_API_KEY`` — nobody drives the ``aws``/``terraform``/
|
||||
# ``boto3`` toolchain off it, so stripping it from terminal/execute_code
|
||||
# subprocesses costs no user capability.
|
||||
#
|
||||
# The GENERAL AWS credential chain (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY,
|
||||
# AWS_SESSION_TOKEN, AWS_PROFILE, and the config/role pointers) is INTENTIONALLY
|
||||
# left inheritable. Per SECURITY.md §3.2 the local terminal is the user's
|
||||
# trusted operator shell; the agent having the same general AWS access the
|
||||
# user's own shell has is the intended posture, not a leak. Hard-blocklisting
|
||||
# those vars would (a) regress every user who runs aws/terraform/cdk/boto3 in
|
||||
# the agent terminal — not just Bedrock users, since the registry is iterated
|
||||
# unconditionally — and (b) be unrecoverable, because env_passthrough.py
|
||||
# refuses to re-allow anything in this blocklist (GHSA-rhgp-j443-p4rf). See
|
||||
# issue #32314 discussion.
|
||||
_AWS_SDK_CREDENTIAL_ENV_VARS = frozenset({
|
||||
"AWS_ACCESS_KEY_ID",
|
||||
"AWS_SECRET_ACCESS_KEY",
|
||||
"AWS_SESSION_TOKEN",
|
||||
"AWS_SECURITY_TOKEN",
|
||||
"AWS_PROFILE",
|
||||
"AWS_DEFAULT_PROFILE",
|
||||
"AWS_SHARED_CREDENTIALS_FILE",
|
||||
"AWS_CONFIG_FILE",
|
||||
"AWS_WEB_IDENTITY_TOKEN_FILE",
|
||||
"AWS_ROLE_ARN",
|
||||
"AWS_ROLE_SESSION_NAME",
|
||||
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
|
||||
"AWS_CONTAINER_CREDENTIALS_FULL_URI",
|
||||
"AWS_CONTAINER_AUTHORIZATION_TOKEN",
|
||||
"AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE",
|
||||
"AWS_BEARER_TOKEN_BEDROCK",
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue