security: strip SESSION_API_KEY from subprocess environment

Prevent LLM-driven agents from accessing SESSION_API_KEY via terminal
commands. This credential grants access to user secrets via the SaaS API
and must remain isolated to the SDK's Python process.

- Add SESSION_API_KEY to _SENSITIVE_ENV_VARS in sanitized_env()
- Add security tests verifying terminal tool cannot access the key

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
openhands
2026-03-23 17:53:07 +00:00
parent 13a4aa6551
commit 8e797ec370
2 changed files with 96 additions and 0 deletions

View File

@@ -11,6 +11,12 @@ from openhands.sdk.logger import get_logger
logger = get_logger(__name__)
# Env vars that should not be exposed to subprocesses (e.g., bash commands
# executed by the agent). These credentials allow access to user secrets via
# the SaaS API and must remain isolated to the SDK's Python process.
_SENSITIVE_ENV_VARS = frozenset({"SESSION_API_KEY"})
def sanitized_env(
env: Mapping[str, str] | None = None,
) -> dict[str, str]:
@@ -19,6 +25,10 @@ def sanitized_env(
PyInstaller-based binaries rewrite ``LD_LIBRARY_PATH`` so their vendored
libraries win. This function restores the original value so that subprocess
will not use them.
Sensitive environment variables (e.g., ``SESSION_API_KEY``) are stripped
to prevent LLM-driven agents from accessing credentials via terminal
commands.
"""
base_env: dict[str, str]
@@ -27,6 +37,10 @@ def sanitized_env(
else:
base_env = dict(env)
# Strip sensitive env vars to prevent agent access via bash commands
for key in _SENSITIVE_ENV_VARS:
base_env.pop(key, None)
if "LD_LIBRARY_PATH_ORIG" in base_env:
origin = base_env["LD_LIBRARY_PATH_ORIG"]
if origin:

View File

@@ -488,3 +488,85 @@ async def test_search_pagination(bash_service):
page1_ids = {event.id for event in page1.items}
page2_ids = {event.id for event in page2.items}
assert len(page1_ids.intersection(page2_ids)) == 0 # No overlap
@pytest.mark.asyncio
async def test_terminal_does_not_expose_session_api_key(bash_service, monkeypatch):
"""Verify SESSION_API_KEY is not accessible to bash commands executed by the terminal.
This is a security test: SESSION_API_KEY grants access to user secrets via the
SaaS API. If an LLM-driven agent could read this env var via terminal commands,
it could exfiltrate all user secrets. The sanitized_env() function must strip
this variable before passing the environment to subprocesses.
"""
# Simulate the automation service injecting SESSION_API_KEY into os.environ
secret_value = "super-secret-session-key-12345"
monkeypatch.setenv("SESSION_API_KEY", secret_value)
collector = EventCollector()
await bash_service.subscribe_to_events(collector)
# An agent might try to read the env var via echo or printenv
request = ExecuteBashRequest(
command='echo "SESSION_API_KEY=$SESSION_API_KEY"',
cwd="/tmp",
)
command, task = await bash_service.start_bash_command(request)
await task
# Collect the output
assert len(collector.outputs) >= 1
combined_stdout = "".join(
output.stdout or "" for output in sorted(collector.outputs, key=lambda x: x.order)
)
# The secret value should NOT appear in the output
assert secret_value not in combined_stdout, (
f"SESSION_API_KEY was exposed to terminal command! Output: {combined_stdout}"
)
# The env var should be empty/unset
assert "SESSION_API_KEY=$" in combined_stdout or "SESSION_API_KEY=\n" in combined_stdout, (
f"SESSION_API_KEY should be unset in subprocess. Output: {combined_stdout}"
)
@pytest.mark.asyncio
async def test_terminal_does_not_expose_session_api_key_via_env_command(
bash_service, monkeypatch
):
"""Verify SESSION_API_KEY doesn't appear in 'env' command output.
An agent might run 'env' or 'printenv' to discover available environment
variables. SESSION_API_KEY must not be visible.
"""
secret_value = "another-secret-key-67890"
monkeypatch.setenv("SESSION_API_KEY", secret_value)
# Also set a safe var to confirm env command works
monkeypatch.setenv("SAFE_TEST_VAR", "visible-value")
collector = EventCollector()
await bash_service.subscribe_to_events(collector)
request = ExecuteBashRequest(
command="env | grep -E '(SESSION_API_KEY|SAFE_TEST_VAR)' || true",
cwd="/tmp",
)
command, task = await bash_service.start_bash_command(request)
await task
assert len(collector.outputs) >= 1
combined_stdout = "".join(
output.stdout or "" for output in sorted(collector.outputs, key=lambda x: x.order)
)
# SESSION_API_KEY should not appear at all
assert "SESSION_API_KEY" not in combined_stdout, (
f"SESSION_API_KEY appeared in env output! Output: {combined_stdout}"
)
assert secret_value not in combined_stdout, (
f"Secret value leaked! Output: {combined_stdout}"
)
# But SAFE_TEST_VAR should be visible (confirms env command worked)
assert "SAFE_TEST_VAR=visible-value" in combined_stdout, (
f"Safe var not found - env command may have failed. Output: {combined_stdout}"
)