mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
fix(github-copilot): preserve configured endpoint trust
This commit is contained in:
parent
b5f11bc4b2
commit
e552f53f80
2 changed files with 14 additions and 71 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue