Skip to content

Commit d516796

Browse files
committed
fix: restrict Streamlit MCP test subprocess env
1 parent d761ed4 commit d516796

2 files changed

Lines changed: 47 additions & 12 deletions

File tree

peak_assistant/streamlit/util/helpers.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,14 @@
2323
from typing import List, Dict, Any, Optional, Tuple
2424
from autogen_agentchat.messages import TextMessage, UserMessage
2525
import streamlit as st
26-
import hashlib
2726
import html
2827
import json
2928
import os
3029
import secrets
3130
import tempfile
3231
import time
3332
import logging
34-
from urllib.parse import urlparse, urljoin
33+
from urllib.parse import urlparse
3534
from pathlib import Path
3635

3736
# Import MCP configuration classes from centralized location
@@ -54,6 +53,39 @@
5453
"token",
5554
}
5655

56+
MCP_SUBPROCESS_DEFAULT_ENV_VARS = (
57+
"PATH",
58+
"HOME",
59+
"USER",
60+
"LOGNAME",
61+
"SHELL",
62+
"TMPDIR",
63+
"TEMP",
64+
"TMP",
65+
"SystemRoot",
66+
"COMSPEC",
67+
"PATHEXT",
68+
)
69+
70+
71+
def build_mcp_subprocess_env(server_env: Optional[Dict[str, str]] = None) -> Dict[str, str]:
72+
"""Build a minimal environment for stdio MCP subprocesses.
73+
74+
Streamlit loads .env secrets into the process environment, so copying all of
75+
os.environ into test subprocesses can expose unrelated LLM API keys, OAuth
76+
secrets, and other PEAK credentials to untrusted MCP packages. Keep only the
77+
small set of platform defaults needed to locate and run local commands, then
78+
add the explicit per-server environment from mcp_servers.json.
79+
"""
80+
env = {
81+
key: os.environ[key]
82+
for key in MCP_SUBPROCESS_DEFAULT_ENV_VARS
83+
if key in os.environ
84+
}
85+
if server_env:
86+
env.update(server_env)
87+
return env
88+
5789

5890
def validate_and_escape_oauth_url(url: str) -> Optional[str]:
5991
"""
@@ -725,10 +757,8 @@ async def test_mcp_connection(server_name: str, server_config: MCPServerConfig)
725757
try:
726758
from autogen_ext.tools.mcp import McpWorkbench, StdioServerParams
727759

728-
# Build complete subprocess environment (mirrors CLI in mcp_config.py)
729-
env = os.environ.copy()
730-
if server_config.env:
731-
env.update(server_config.env)
760+
# Build a minimal subprocess environment plus explicit server env.
761+
env = build_mcp_subprocess_env(server_config.env)
732762

733763
server_params = StdioServerParams(
734764
command=server_config.command,
@@ -1147,7 +1177,7 @@ def get_agent_config_data() -> List[Dict[str, str]]:
11471177
try:
11481178
provider_config = loader.get_provider_config(provider_name)
11491179
provider_type = provider_config["type"]
1150-
except:
1180+
except Exception:
11511181
provider_type = "unknown"
11521182

11531183
agent_data.append({

tests/unit_tests/test_streamlit_helpers_mcp_bugs.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
Tests for Streamlit helpers MCP configuration bugs.
2525
2626
Bug 1: load_mcp_server_configs() does not interpolate ${ENV_VAR} patterns.
27-
Bug 2: test_mcp_connection() does not include system env vars in subprocess.
27+
Bug 2: test_mcp_connection() leaks full process env to subprocesses.
2828
"""
2929

3030
import json
@@ -139,12 +139,13 @@ def test_missing_env_var_without_default_returns_empty(self, monkeypatch):
139139

140140

141141
class TestMcpConnectionSubprocessEnv:
142-
"""Bug 2: test_mcp_connection() should pass full system env to subprocess"""
142+
"""Bug 2: test_mcp_connection() should avoid leaking full process env"""
143143

144144
@pytest.mark.asyncio
145145
async def test_subprocess_env_includes_system_path(self, monkeypatch):
146-
"""StdioServerParams.env should contain both custom and system env vars"""
146+
"""StdioServerParams.env should contain custom env and safe default vars"""
147147
monkeypatch.setenv("PATH", "/usr/bin:/usr/local/bin")
148+
monkeypatch.setenv("UNRELATED_OAUTH_CLIENT_SECRET", "oauth-secret-not-for-mcp")
148149

149150
config = MCPServerConfig(
150151
name="test-server",
@@ -175,11 +176,13 @@ def capture_workbench(server_params):
175176
assert params.env["CUSTOM_KEY"] == "custom_val"
176177
assert "PATH" in params.env
177178
assert params.env["PATH"] == "/usr/bin:/usr/local/bin"
179+
assert "UNRELATED_OAUTH_CLIENT_SECRET" not in params.env
178180

179181
@pytest.mark.asyncio
180-
async def test_subprocess_env_with_no_config_env_gets_system_env(self, monkeypatch):
181-
"""Even with env=None in config, subprocess should get system env"""
182+
async def test_subprocess_env_with_no_config_env_gets_safe_default_env(self, monkeypatch):
183+
"""Even with env=None in config, subprocess should only get safe defaults"""
182184
monkeypatch.setenv("PATH", "/usr/bin:/usr/local/bin")
185+
monkeypatch.setenv("PEAK_GLOBAL_SECRET", "global-secret-not-for-mcp")
183186

184187
config = MCPServerConfig(
185188
name="test-server",
@@ -206,3 +209,5 @@ def capture_workbench(server_params):
206209
assert success is True
207210
params = captured_params["server_params"]
208211
assert "PATH" in params.env
212+
assert params.env["PATH"] == "/usr/bin:/usr/local/bin"
213+
assert "PEAK_GLOBAL_SECRET" not in params.env

0 commit comments

Comments
 (0)