"""Regression tests for proxy-level env compartmentalization. This is the structural defense against agent-side secret exfiltration via mcp tools, like core's bash. """ from mcp_proxy.commands.mcp import _build_subprocess_env def test_declared_credential_is_forwarded(tmp_path, monkeypatch): """A service that declares a credential by exact name in its mcp.json ``env`` list does receive it — opt-in bypasses the denylist.""" monkeypatch.setenv("BRAVE_API_KEY", "secret-for-web") env = _build_subprocess_env(tmp_path, server_name="web", declared_secrets=["BRAVE_API_KEY"]) assert env["BRAVE_API_KEY"] == "secret-for-web" def test_secret_isolation_between_services(tmp_path, monkeypatch): """Same proxy env, two services with different declarations — only the declaring service sees the secret.""" monkeypatch.setenv("BRAVE_API_KEY", "secret-do-not-leak") web_env = _build_subprocess_env(tmp_path, server_name="web", declared_secrets=["BRAVE_API_KEY"]) core_env = _build_subprocess_env(tmp_path, server_name="core", declared_secrets=()) assert "BRAVE_API_KEY" in web_env assert "BRAVE_API_KEY" not in core_env def test_credential_shaped_vars_are_dropped(tmp_path, monkeypatch): pollute = { # KEY "BRAVE_API_KEY": "x", "MY_PRIVATE_KEY_PATH": "/etc/key", "WORLDBENCH_FOO_API_KEY": "x", "GITHUB_TOKEN": "ghp_x", "AWS_SECRET_ACCESS_KEY": "aws-x", "stripe_secret": "x", # case-insensitive "DB_PASSWORD": "hunter2", "MYSQL_PASSWD": "old-school-x", "SSH_PASSPHRASE": "x", "GPG_PASSPHRASE": "x", "GOOGLE_CREDENTIALS": "json-x", "OAUTH_CLIENT_ID": "x", "BEARER_TOKEN": "x", "JWT_SECRET": "x", "JWT_SIGNING_KEY": "x", "SESSION_COOKIE": "x", "AUTH_COOKIE": "x", "TOTP_SECRET": "x", "HOTP_KEY": "x", "DATABASE_URL": "postgres://app_user:hunter2@db.internal:5432/app", "REDIS_URL": "redis://:hunter2@cache.internal:6379/0", "PIP_INDEX_URL": "https://user:token@pypi.example.com/simple", "UV_INDEX_URL": "https://user:token@uv.example.com/simple", "JDBC_DATABASE_URL": "jdbc:postgresql://app:hunter2@db.internal/app", "JDBC_MYSQL_URL": "jdbc:mysql://u:p@db:3306/x", "GIT_REMOTE_URL": "https://ghp_xxxxxxxxxxxx@github.com/org/repo", "REGISTRY_URL": "https://pypi-token-here@pypi.example.com/simple", } for k, v in pollute.items(): monkeypatch.setenv(k, v) env = _build_subprocess_env(tmp_path, server_name="core", declared_secrets=()) for k in pollute: assert k not in env, f"{k} leaked despite credential-shaped name" def test_innocuous_vars_pass_through(tmp_path, monkeypatch): """Default-allow: anything without a credential keyword is forwarded. This is what makes the simplified model work — we don't have to enumerate every system var a subprocess might legitimately need.""" monkeypatch.setenv("PATH", "/usr/bin:/bin") monkeypatch.setenv("HOME", "/home/model") monkeypatch.setenv("LANG", "en_US.UTF-8") monkeypatch.setenv("WORLDBENCH_TASK_ID", "task-123") monkeypatch.setenv("WORLDBENCH_TOOL_SETS", "core_read") monkeypatch.setenv("RANDOM_CUSTOM_VAR", "value") monkeypatch.setenv("BRAVE_SEARCH_URL", "https://api.search.brave.com/res/v1/web/search") monkeypatch.setenv("PUBLIC_API_BASE", "https://api.example.com/v1") monkeypatch.setenv("DATABASE_URL", "postgres://localhost/dev") # no userinfo env = _build_subprocess_env(tmp_path, server_name="core", declared_secrets=()) assert env["PATH"] == "/usr/bin:/bin" assert env["HOME"] == "/home/model" assert env["LANG"] == "en_US.UTF-8" assert env["WORLDBENCH_TASK_ID"] == "task-123" assert env["WORLDBENCH_TOOL_SETS"] == "core_read" assert env["RANDOM_CUSTOM_VAR"] == "value" assert env["BRAVE_SEARCH_URL"] == "https://api.search.brave.com/res/v1/web/search" assert env["PUBLIC_API_BASE"] == "https://api.example.com/v1" assert env["DATABASE_URL"] == "postgres://localhost/dev" def test_url_with_userinfo_can_be_explicitly_declared(tmp_path, monkeypatch): """A service that legitimately needs a credentialed URL can opt in by declaring the var name in mcp.json's ``secrets`` — declared names bypass both the name-shape and value-shape checks.""" monkeypatch.setenv("DATABASE_URL", "postgres://app_user:hunter2@db.internal:5432/app") db_user_env = _build_subprocess_env(tmp_path, server_name="my-db-service", declared_secrets=["DATABASE_URL"]) core_env = _build_subprocess_env(tmp_path, server_name="core", declared_secrets=()) assert "DATABASE_URL" in db_user_env assert "DATABASE_URL" not in core_env