fix(providers): recognize OpenAI data-residency hosts via one shared predicate
Pointing openai-api at OpenAI's documented regional hosts (us.api.openai.com / eu.api.openai.com, mandatory for customers with data-residency obligations) silently degraded Hermes because three subsystems tested 'is this OpenAI' with exact-hostname equality against api.openai.com. Adds providers.is_official_openai_host(): canonical host plus dot-suffix subdomains of api.openai.com, hostname-parsed only. Lookalike hosts (api.openai.com.attacker.test) and path spoofs (proxy.test/api.openai.com/v1) stay rejected, preserving the #32243 hardening: a genuine *.api.openai.com subdomain requires control of openai.com DNS. host_mandated_api_mode() now routes through the predicate, so regional hosts mandate codex_responses exactly like the canonical host.
This commit is contained in:
parent
dc87d15586
commit
564e9b90af
|
|
@ -578,6 +578,25 @@ def is_routing_aggregator(provider: str) -> bool:
|
|||
return is_aggregator(provider_norm)
|
||||
|
||||
|
||||
def is_official_openai_host(base_url: str) -> bool:
|
||||
"""True when *base_url* points at OpenAI's official API host family.
|
||||
|
||||
Matches the canonical host (``api.openai.com``) and OpenAI's documented
|
||||
data-residency / regional hosts (``us.api.openai.com``,
|
||||
``eu.api.openai.com``, and any future ``<region>.api.openai.com``) —
|
||||
those serve the same API surface with the same transport requirements
|
||||
and the same access-scoped ``/v1/models`` listing.
|
||||
|
||||
Hostname-parsed matching only — never substring — so lookalike hosts
|
||||
(``api.openai.com.attacker.test``) and path-segment spoofs
|
||||
(``proxy.test/api.openai.com/v1``) are rejected. A genuine
|
||||
``*.api.openai.com`` subdomain requires control of openai.com DNS, so
|
||||
the dot-suffix match does not reopen the #32243 spoofing hole.
|
||||
"""
|
||||
hostname = base_url_hostname(base_url)
|
||||
return hostname == "api.openai.com" or hostname.endswith(".api.openai.com")
|
||||
|
||||
|
||||
def host_mandated_api_mode(base_url: str = "") -> Optional[str]:
|
||||
"""Return the wire protocol a specific endpoint *requires*, or None.
|
||||
|
||||
|
|
@ -605,7 +624,11 @@ def host_mandated_api_mode(base_url: str = "") -> Optional[str]:
|
|||
return "anthropic_messages"
|
||||
if hostname == "api.anthropic.com" or url_lower.endswith("/anthropic"):
|
||||
return "anthropic_messages"
|
||||
if hostname == "api.openai.com":
|
||||
# Official OpenAI host family: canonical + data-residency regional hosts
|
||||
# (us./eu.api.openai.com) all mandate the Responses API for reasoning
|
||||
# models with tools. Shared predicate keeps this lane in lockstep with
|
||||
# catalog filtering and listing authority.
|
||||
if is_official_openai_host(base_url):
|
||||
return "codex_responses"
|
||||
if hostname.startswith("bedrock-runtime.") and base_url_host_matches(base_url, "amazonaws.com"):
|
||||
return "bedrock_converse"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
"""Security + parity contract for ``is_official_openai_host``.
|
||||
|
||||
One predicate decides "is this endpoint OpenAI's official API surface?"
|
||||
for every lane that branches on it: transport mandates
|
||||
(``host_mandated_api_mode``), URL auto-detection in the runtime resolver,
|
||||
model-catalog filtering, and live-listing authority. OpenAI's documented
|
||||
data-residency hosts (``us.api.openai.com``, ``eu.api.openai.com``, and any
|
||||
future ``<region>.api.openai.com``) are the same API surface as the
|
||||
canonical host and must match; lookalike/spoof hosts must not (#32243).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from hermes_cli.providers import is_official_openai_host
|
||||
|
||||
|
||||
class TestOfficialHosts:
|
||||
@pytest.mark.parametrize(
|
||||
"url",
|
||||
[
|
||||
"https://api.openai.com/v1",
|
||||
"https://api.openai.com",
|
||||
"https://us.api.openai.com/v1",
|
||||
"https://eu.api.openai.com/v1",
|
||||
"https://US.api.OpenAI.com/v1", # case-insensitive hostname
|
||||
"https://in.api.openai.com/v1", # future regional variants
|
||||
],
|
||||
)
|
||||
def test_official_hosts_match(self, url):
|
||||
assert is_official_openai_host(url) is True
|
||||
|
||||
|
||||
class TestSpoofRejection:
|
||||
@pytest.mark.parametrize(
|
||||
"url",
|
||||
[
|
||||
# Lookalike host suffix: registrable domain is attacker.test.
|
||||
"https://api.openai.com.attacker.test/v1",
|
||||
"https://us.api.openai.com.attacker.test/v1",
|
||||
# Path-segment spoofing: host is proxy.test.
|
||||
"https://proxy.test/api.openai.com/v1",
|
||||
"https://proxy.test/us.api.openai.com/v1",
|
||||
# Prefix tricks that are NOT dot-separated subdomains of
|
||||
# api.openai.com (fooapi.openai.com is an openai.com host but
|
||||
# not the official API host family this predicate is scoped to).
|
||||
"https://evilapi.openai.com.attacker.test/v1",
|
||||
"https://fooapi.openai.com/v1",
|
||||
# Unrelated hosts.
|
||||
"https://openrouter.ai/api/v1",
|
||||
"https://api.anthropic.com/v1",
|
||||
"",
|
||||
],
|
||||
)
|
||||
def test_spoof_and_unrelated_hosts_rejected(self, url):
|
||||
assert is_official_openai_host(url) is False
|
||||
Loading…
Reference in New Issue