From e552f53f802a5c8e65ef613961ff4bc75522cdae Mon Sep 17 00:00:00 2001 From: codgician <15964984+codgician@users.noreply.github.com> Date: Tue, 28 Jul 2026 17:02:40 +0800 Subject: [PATCH] fix(github-copilot): preserve configured endpoint trust --- litellm/llms/github_copilot/authenticator.py | 30 ++-------- .../test_github_copilot_authenticator.py | 55 +++---------------- 2 files changed, 14 insertions(+), 71 deletions(-) diff --git a/litellm/llms/github_copilot/authenticator.py b/litellm/llms/github_copilot/authenticator.py index 9f7c33c4487..495ede79bfd 100644 --- a/litellm/llms/github_copilot/authenticator.py +++ b/litellm/llms/github_copilot/authenticator.py @@ -37,28 +37,8 @@ def _https_hostname(url: str) -> str | None: return parsed_url.hostname.lower() -def _configured_allowed_api_hosts() -> frozenset[str]: - configured_hosts = os.getenv("GITHUB_COPILOT_ALLOWED_API_HOSTS", "") - return frozenset(host.strip().lower() for host in configured_hosts.split(",") if host.strip()) - - -def _configured_oauth_hosts() -> tuple[str, ...]: - oauth_urls = ( - os.getenv("GITHUB_COPILOT_DEVICE_CODE_URL", DEFAULT_GITHUB_DEVICE_CODE_URL), - os.getenv("GITHUB_COPILOT_ACCESS_TOKEN_URL", DEFAULT_GITHUB_ACCESS_TOKEN_URL), - ) - return tuple(hostname for url in oauth_urls if (hostname := _https_hostname(url)) is not None) - - -def _is_trusted_api_base(api_base: str) -> bool: - hostname = _https_hostname(api_base) - if hostname is None: - return False - if hostname == "githubcopilot.com" or hostname.endswith(".githubcopilot.com"): - return True - if hostname in _configured_allowed_api_hosts(): - return True - return any(hostname == f"copilot-api.{oauth_host}" for oauth_host in _configured_oauth_hosts()) +def _is_secure_api_base(api_base: str) -> bool: + return _https_hostname(api_base) is not None class Authenticator: @@ -129,9 +109,11 @@ class Authenticator: for source, candidate in candidates: if candidate is None: continue - if _is_trusted_api_base(candidate): + if _is_secure_api_base(candidate): return candidate - verbose_logger.warning(f"Ignoring {source} because it is not a trusted HTTPS GitHub Copilot endpoint") + verbose_logger.warning( + f"Ignoring {source} because it must be an HTTPS URL without credentials, query, or fragment" + ) return None def _ensure_token_dir(self) -> None: diff --git a/tests/test_litellm/llms/github_copilot/test_github_copilot_authenticator.py b/tests/test_litellm/llms/github_copilot/test_github_copilot_authenticator.py index 0e6a7fea489..49906d3dd62 100644 --- a/tests/test_litellm/llms/github_copilot/test_github_copilot_authenticator.py +++ b/tests/test_litellm/llms/github_copilot/test_github_copilot_authenticator.py @@ -55,10 +55,10 @@ class TestGitHubCopilotAuthenticator: def test_get_api_base_prefers_environment(self, authenticator): with patch.dict( os.environ, - {"GITHUB_COPILOT_API_BASE": "https://api.enterprise.githubcopilot.com"}, + {"GITHUB_COPILOT_API_BASE": "https://configured.example.com"}, clear=True, ): - assert authenticator.get_api_base() == "https://api.enterprise.githubcopilot.com" + assert authenticator.get_api_base() == "https://configured.example.com" @pytest.mark.parametrize( "api_base", @@ -67,7 +67,6 @@ class TestGitHubCopilotAuthenticator: "https://user:password@api.githubcopilot.com", "https://api.githubcopilot.com?tenant=example", "https://api.githubcopilot.com#fragment", - "https://attacker.example.com", ), ) def test_get_api_base_rejects_insecure_configuration(self, authenticator, api_base): @@ -78,72 +77,34 @@ class TestGitHubCopilotAuthenticator: assert authenticator.get_api_base() is None mock_warning.assert_called_once_with( - "Ignoring GITHUB_COPILOT_API_BASE because it is not a trusted HTTPS GitHub Copilot endpoint" + "Ignoring GITHUB_COPILOT_API_BASE because it must be an HTTPS URL without credentials, query, or fragment" ) def test_get_api_base_uses_default_when_unconfigured(self, authenticator): with patch.dict(os.environ, {}, clear=True): assert authenticator.get_api_base() is None - def test_get_api_base_trusts_enterprise_oauth_domain(self, authenticator): - environment = { - "GITHUB_COPILOT_API_BASE": "https://copilot-api.company.ghe.com", - "GITHUB_COPILOT_DEVICE_CODE_URL": "https://company.ghe.com/login/device/code", - "GITHUB_COPILOT_ACCESS_TOKEN_URL": "https://company.ghe.com/login/oauth/access_token", - } - with patch.dict(os.environ, environment, clear=True): - assert authenticator.get_api_base() == "https://copilot-api.company.ghe.com" - - def test_get_api_base_rejects_other_oauth_subdomains(self, authenticator): - environment = { - "GITHUB_COPILOT_API_BASE": "https://evil.company.ghe.com", - "GITHUB_COPILOT_DEVICE_CODE_URL": "https://company.ghe.com/login/device/code", - "GITHUB_COPILOT_ACCESS_TOKEN_URL": "https://company.ghe.com/login/oauth/access_token", - } - with ( - patch.dict(os.environ, environment, clear=True), - patch("litellm.llms.github_copilot.authenticator.verbose_logger.warning") as mock_warning, - ): - assert authenticator.get_api_base() is None - - mock_warning.assert_called_once_with( - "Ignoring GITHUB_COPILOT_API_BASE because it is not a trusted HTTPS GitHub Copilot endpoint" - ) - - def test_get_api_base_trusts_explicit_allowed_host(self, authenticator): - environment = { - "GITHUB_COPILOT_API_BASE": "https://copilot-proxy.example.com", - "GITHUB_COPILOT_ALLOWED_API_HOSTS": "copilot-proxy.example.com", - } - with patch.dict(os.environ, environment, clear=True): - assert authenticator.get_api_base() == "https://copilot-proxy.example.com" - def test_get_api_base_prefers_trusted_deployment_endpoint(self, authenticator): with patch.dict( os.environ, - {"GITHUB_COPILOT_API_BASE": "https://api.individual.githubcopilot.com"}, + {"GITHUB_COPILOT_API_BASE": "https://configured.example.com"}, clear=True, ): - assert ( - authenticator.get_api_base("https://api.enterprise.githubcopilot.com") - == "https://api.enterprise.githubcopilot.com" - ) + assert authenticator.get_api_base("https://deployment.example.com") == "https://deployment.example.com" def test_get_api_base_falls_back_from_untrusted_deployment_endpoint(self, authenticator): with ( patch.dict( os.environ, - {"GITHUB_COPILOT_API_BASE": "https://api.individual.githubcopilot.com"}, + {"GITHUB_COPILOT_API_BASE": "https://configured.example.com"}, clear=True, ), patch("litellm.llms.github_copilot.authenticator.verbose_logger.warning") as mock_warning, ): - assert ( - authenticator.get_api_base("https://attacker.example.com") == "https://api.individual.githubcopilot.com" - ) + assert authenticator.get_api_base("http://attacker.example.com") == "https://configured.example.com" mock_warning.assert_called_once_with( - "Ignoring deployment api_base because it is not a trusted HTTPS GitHub Copilot endpoint" + "Ignoring deployment api_base because it must be an HTTPS URL without credentials, query, or fragment" ) def test_get_github_headers(self, authenticator):