fix(#37878): scrub operator environment before launching cua-driver MCP

- Use _sanitize_subprocess_env() to filter Hermes-managed credentials
  from the cua-driver subprocess environment (issue #37878)
- Prevents credential exfiltration to the third-party cua-driver binary
- Aligns with existing pattern used by browser-tool and other tools
- Add regression test to verify environment sanitization

The cua-driver is a lower-trust MCP subprocess per SECURITY.md §2.3.
Its inherited environment is now scrubbed by default, removing provider
API keys, gateway tokens, and platform credentials that should not leak
to third-party binaries.

Fixes #37878
This commit is contained in:
Luke The Dev 2026-06-07 11:22:31 -05:00 committed by Teknium
parent b39ec2fc37
commit 2e5c04aaf7
2 changed files with 76 additions and 1 deletions

View file

@ -1450,3 +1450,77 @@ class TestFocusAppFilterNoMatch:
assert res.ok is True
assert backend._active_pid == 200
assert backend._active_window_id == 2
class TestCuaEnvironmentScrubbing:
"""Verify that cua-driver subprocess environment is sanitized (issue #37878)."""
def test_cua_session_sanitizes_provider_env_vars(self):
"""_CuaDriverSession._aenter() must sanitize sensitive env vars.
The cua-driver MCP subprocess should not inherit Hermes-managed credentials
or other sensitive environment variables only runtime-required vars.
This is a regression test for issue #37878.
"""
from unittest.mock import MagicMock, patch, AsyncMock
from tools.computer_use.cua_backend import _CuaDriverSession, _AsyncBridge
import asyncio
bridge = _AsyncBridge()
session = _CuaDriverSession(bridge)
captured_env = {}
async def test_aenter():
# Set up test environment with both safe and blocked vars
test_env = {
"OPENAI_API_KEY": "sk-secret", # blocked
"PATH": "/usr/bin:/bin", # safe
"HOME": "/home/user", # safe
"SAFE_VAR": "allowed", # safe
}
with patch.dict(os.environ, test_env, clear=True):
with patch("tools.computer_use.cua_backend.cua_driver_binary_available",
return_value=True):
# Mock StdioServerParameters to capture the env arg
def capture_env(**kwargs):
captured_env.update(kwargs.get("env", {}))
# Return mock that works with async context manager
mock = MagicMock()
mock.__aenter__ = AsyncMock(return_value=(MagicMock(), MagicMock()))
mock.__aexit__ = AsyncMock(return_value=None)
return mock
with patch("mcp.StdioServerParameters", side_effect=capture_env), \
patch("mcp.client.stdio.stdio_client") as mock_stdio, \
patch("mcp.ClientSession") as mock_session_class, \
patch("contextlib.AsyncExitStack"):
# Setup mocks for stdio_client and ClientSession
mock_read = MagicMock()
mock_write = MagicMock()
mock_stdio.return_value.__aenter__ = AsyncMock(
return_value=(mock_read, mock_write))
mock_stdio.return_value.__aexit__ = AsyncMock(return_value=None)
mock_session = MagicMock()
mock_session.initialize = AsyncMock()
mock_session_class.return_value.__aenter__ = AsyncMock(
return_value=mock_session)
mock_session_class.return_value.__aexit__ = AsyncMock(return_value=None)
try:
await session._aenter()
except Exception:
pass # Mocks may raise, but env should be captured
asyncio.run(test_aenter())
# Verify blocked credentials are not in the passed env
assert "OPENAI_API_KEY" not in captured_env, \
"OPENAI_API_KEY should be stripped from cua-driver subprocess"
# Verify PATH is preserved (safe var)
assert "PATH" in captured_env or "SAFE_VAR" in captured_env, \
"At least one safe environment variable should be preserved"

View file

@ -270,6 +270,7 @@ class _CuaDriverSession:
from contextlib import AsyncExitStack
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from tools.environments.local import _sanitize_subprocess_env
if not cua_driver_binary_available():
raise RuntimeError(cua_driver_install_hint())
@ -277,7 +278,7 @@ class _CuaDriverSession:
params = StdioServerParameters(
command=_CUA_DRIVER_CMD,
args=_CUA_DRIVER_ARGS,
env={**os.environ},
env=_sanitize_subprocess_env(dict(os.environ)),
)
stack = AsyncExitStack()
read, write = await stack.enter_async_context(stdio_client(params))