fix(github-copilot): preserve configured endpoint trust

This commit is contained in:
codgician 2026-07-28 17:02:40 +08:00
parent b5f11bc4b2
commit e552f53f80
No known key found for this signature in database
2 changed files with 14 additions and 71 deletions

View file

@ -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:

View file

@ -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):