From a98c8eeed1911a8406355a19274d30a193b72635 Mon Sep 17 00:00:00 2001 From: KaliShodan <25535214+jbbottoms@users.noreply.github.com> Date: Sat, 11 Jul 2026 21:06:02 -0500 Subject: [PATCH] fix(gateway): isolate deferred platform imports from os.environ leaks microsoft-teams-apps calls load_dotenv(find_dotenv(usecwd=True)) at import time, which can pull a root-profile .env into every gateway process during plugin_entries() discovery and break profile secret isolation. Snapshot/restore os.environ around deferred loaders, and honor explicit api_server enabled:false the same way _enable_from_env does for other platforms. Fixes #62935 --- gateway/platform_registry.py | 31 ++++++++++++++++++-- tests/gateway/test_platform_registry.py | 38 +++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/gateway/platform_registry.py b/gateway/platform_registry.py index b3c19af1bcbf6..99ca598966374 100644 --- a/gateway/platform_registry.py +++ b/gateway/platform_registry.py @@ -29,12 +29,36 @@ Usage (gateway side): """ import logging +import os +from contextlib import contextmanager from dataclasses import dataclass, field -from typing import Any, Awaitable, Callable, Optional +from typing import Any, Awaitable, Callable, Iterator, Optional logger = logging.getLogger(__name__) +@contextmanager +def _isolated_os_environ() -> Iterator[None]: + """Snapshot and restore ``os.environ`` around a deferred platform import. + + Some third-party SDKs (notably ``microsoft-teams-apps``) call + ``load_dotenv(find_dotenv(usecwd=True))`` at module import time. That + walks up from the process cwd and mutates the process-global environ — + leaking root-profile secrets into secondary profiles and bypassing + Hermes's own dotenv / secret-scope rules (#62935). + + Deferred loaders are import-and-register only; any environ mutation they + cause is treated as an unwanted side effect and discarded. + """ + saved = dict(os.environ) + try: + yield + finally: + os.environ.clear() + os.environ.update(saved) + + + @dataclass class PlatformEntry: """Metadata and factory for a single platform adapter.""" @@ -205,7 +229,10 @@ class PlatformRegistry: if loader is None: return try: - loader() + # Isolate environ so import-time dotenv / env mutation cannot leak + # across profiles or override explicit YAML disables (#62935). + with _isolated_os_environ(): + loader() except Exception as e: logger.warning( "Deferred load of platform '%s' failed: %s", diff --git a/tests/gateway/test_platform_registry.py b/tests/gateway/test_platform_registry.py index 71ea088238dc0..846b4c1e4871e 100644 --- a/tests/gateway/test_platform_registry.py +++ b/tests/gateway/test_platform_registry.py @@ -494,3 +494,41 @@ class TestPluginEnablementGate: ) finally: _reg.unregister("myrejectedplat") + + + +class TestDeferredLoadEnvironIsolation: + """Deferred platform imports must not leak os.environ mutations (#62935).""" + + def test_deferred_loader_environ_mutations_are_discarded(self, monkeypatch): + monkeypatch.delenv("API_SERVER_ENABLED", raising=False) + monkeypatch.delenv("LEAKED_FROM_IMPORT", raising=False) + assert "API_SERVER_ENABLED" not in os.environ + + reg = PlatformRegistry() + + def _polluting_loader(): + os.environ["API_SERVER_ENABLED"] = "true" + os.environ["LEAKED_FROM_IMPORT"] = "1" + entry, _ = TestPlatformRegistry()._make_entry("polluter") + reg.register(entry) + + reg.register_deferred("polluter", _polluting_loader) + assert reg.get("polluter") is not None + assert "API_SERVER_ENABLED" not in os.environ + assert "LEAKED_FROM_IMPORT" not in os.environ + + def test_deferred_loader_preserves_preexisting_environ(self, monkeypatch): + monkeypatch.setenv("HERMES_KEEP", "alive") + reg = PlatformRegistry() + + def _loader(): + os.environ["HERMES_KEEP"] = "clobbered" + os.environ["TEMP_LEAK"] = "1" + entry, _ = TestPlatformRegistry()._make_entry("keeper") + reg.register(entry) + + reg.register_deferred("keeper", _loader) + assert reg.get("keeper") is not None + assert os.environ.get("HERMES_KEEP") == "alive" + assert "TEMP_LEAK" not in os.environ