diff --git a/hermes_cli/urllib_security.py b/hermes_cli/urllib_security.py index ae4ab8aff87..29c000985ae 100644 --- a/hermes_cli/urllib_security.py +++ b/hermes_cli/urllib_security.py @@ -64,7 +64,10 @@ class _CrossOriginRequestSanitizer(urllib.request.BaseHandler): # Request processors run in ascending order. Keep this last so an installed # cookie/auth/instrumentation processor cannot re-add a secret after the # redirect handler sanitizes the new Request. - handler_order = 1_000_000_000 + # Infinity is greater than every finite handler order. If an installed + # processor also uses infinity, stable sorting keeps this appended handler + # after it, so sanitization still owns the final request boundary. + handler_order = float("inf") # type: ignore[assignment] def __init__(self, original_url: str) -> None: self._original_origin = url_origin(original_url) diff --git a/tests/hermes_cli/test_urllib_security.py b/tests/hermes_cli/test_urllib_security.py index 32d5ebd187a..d21ac3bb030 100644 --- a/tests/hermes_cli/test_urllib_security.py +++ b/tests/hermes_cli/test_urllib_security.py @@ -329,6 +329,8 @@ def test_installed_request_processor_cannot_resurrect_cross_origin_secret( _RecordingHandler.redirect_to = f"http://localhost:{sink.server_port}/sink" class SecretProcessor(urllib.request.BaseHandler): + handler_order = float("inf") # type: ignore[assignment] + def http_request(self, request): request.add_header("X-Installed-Secret", "must-not-cross") return request @@ -481,6 +483,50 @@ def test_anthropic_profile_drops_x_api_key_on_redirect(monkeypatch): assert headers["accept"] == "application/json" +def test_azure_catalog_probe_drops_api_key_and_bearer_on_redirect(): + from hermes_cli import azure_detect + + source = _server() + sink = _server() + _RecordingHandler.requests = [] + _RecordingHandler.redirect_status = 302 + _RecordingHandler.redirect_to = f"http://localhost:{sink.server_port}/sink" + try: + status, body = azure_detect._http_get_json( + f"http://127.0.0.1:{source.server_port}/redirect", "azure-secret", timeout=3 + ) + finally: + source.shutdown() + sink.shutdown() + + assert status == 200 + assert body == {"data": []} + _, headers = _RecordingHandler.requests[-1] + assert "authorization" not in headers + assert "api-key" not in headers + + +def test_azure_anthropic_probe_drops_api_key_and_bearer_on_redirect(): + from hermes_cli import azure_detect + + sink = _server() + source = ThreadingHTTPServer(("127.0.0.1", 0), _LmStudioSourceHandler) + Thread(target=source.serve_forever, daemon=True).start() + _RecordingHandler.requests = [] + _LmStudioSourceHandler.redirect_to = f"http://localhost:{sink.server_port}/sink" + try: + azure_detect._probe_anthropic_messages( + f"http://127.0.0.1:{source.server_port}", "azure-secret" + ) + finally: + source.shutdown() + sink.shutdown() + + _, headers = _RecordingHandler.requests[-1] + assert "authorization" not in headers + assert "api-key" not in headers + + def test_lmstudio_load_post_drops_bearer_on_redirect(monkeypatch): from hermes_cli import models