From 27a1042b130e3075abe3d0f913dec2f60b59c9c8 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Sat, 11 Jul 2026 11:37:39 +0530 Subject: [PATCH] fix(security): preserve installed urllib policies --- hermes_cli/urllib_security.py | 27 ++++++++++++++-- tests/hermes_cli/test_urllib_security.py | 41 ++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/hermes_cli/urllib_security.py b/hermes_cli/urllib_security.py index 187ddd6fe43..d60625229c3 100644 --- a/hermes_cli/urllib_security.py +++ b/hermes_cli/urllib_security.py @@ -2,6 +2,7 @@ from __future__ import annotations +import copy import urllib.parse import urllib.request from collections.abc import Callable, Iterable @@ -58,18 +59,38 @@ class SafeCredentialRedirectHandler(urllib.request.HTTPRedirectHandler): return redirected +def _secure_opener_from_installed_policy(original_url: str): + """Clone the installed opener's handlers, replacing redirect policy only.""" + installed = getattr(urllib.request, "_opener", None) + if installed is None: + installed = urllib.request.build_opener() + + handlers = [ + copy.copy(handler) + for handler in getattr(installed, "handlers", ()) + if not isinstance(handler, urllib.request.HTTPRedirectHandler) + ] + handlers.append(SafeCredentialRedirectHandler(original_url)) + return urllib.request.build_opener(*handlers) + + def open_credentialed_url( request: urllib.request.Request, *, timeout: float, - opener_factory: Callable[..., Any] = urllib.request.build_opener, + opener_factory: Callable[..., Any] | None = None, ): """Open a request without forwarding credentials across origins. - ``opener_factory`` is an explicit instrumentation/test seam. Security is + The default preserves an application-installed opener's proxy, TLS, + cookies, custom protocol handlers, and instrumentation while replacing its + redirect handler. ``opener_factory`` is an explicit test seam; security is never disabled based on global ``urlopen`` identity. """ - opener = opener_factory(SafeCredentialRedirectHandler(request.full_url)) + if opener_factory is None: + opener = _secure_opener_from_installed_policy(request.full_url) + else: + opener = opener_factory(SafeCredentialRedirectHandler(request.full_url)) return opener.open(request, timeout=timeout) diff --git a/tests/hermes_cli/test_urllib_security.py b/tests/hermes_cli/test_urllib_security.py index da4189a2554..c1e9667c127 100644 --- a/tests/hermes_cli/test_urllib_security.py +++ b/tests/hermes_cli/test_urllib_security.py @@ -254,6 +254,47 @@ def test_explicit_opener_factory_is_instrumentable_without_security_bypass(): assert calls == [("https://models.example.test/models", 7)] +def test_installed_custom_opener_policy_is_preserved(monkeypatch): + opened = [] + + class FooHandler(urllib.request.BaseHandler): + def foo_open(self, request): + opened.append(request.full_url) + return _Response(b"custom") + + installed = urllib.request.build_opener(FooHandler()) + monkeypatch.setattr(urllib.request, "_opener", installed) + + request = urllib.request.Request( + "foo://models.example.test/catalog", headers={"Authorization": "secret"} + ) + with open_credentialed_url(request, timeout=3) as response: + assert response.read() == b"custom" + assert opened == ["foo://models.example.test/catalog"] + + +def test_installed_proxy_handler_is_preserved(monkeypatch): + installed = urllib.request.build_opener( + urllib.request.ProxyHandler({"https": "http://proxy.example.test:8443"}) + ) + monkeypatch.setattr(urllib.request, "_opener", installed) + + from hermes_cli.urllib_security import _secure_opener_from_installed_policy + + secured = _secure_opener_from_installed_policy( + "https://models.example.test/catalog" + ) + proxy_handlers = [ + handler + for handler in getattr(secured, "handlers", ()) + if isinstance(handler, urllib.request.ProxyHandler) + ] + assert proxy_handlers + assert getattr(proxy_handlers[0], "proxies", {}) == { + "https": "http://proxy.example.test:8443" + } + + def test_probe_api_models_drops_custom_credentials_on_wire(): from hermes_cli.models import probe_api_models