fix: sweep remaining provider-URL substring checks across codebase
Completes the hostname-hardening sweep — every substring check against a
provider host in live-routing code is now hostname-based. This closes the
same false-positive class for OpenRouter, GitHub Copilot, Kimi, Qwen,
ChatGPT/Codex, Bedrock, GitHub Models, Vercel AI Gateway, Nous, Z.AI,
Moonshot, Arcee, and MiniMax that the original PR closed for OpenAI, xAI,
and Anthropic.
New helper:
- utils.base_url_host_matches(base_url, domain) — safe counterpart to
'domain in base_url'. Accepts hostname equality and subdomain matches;
rejects path segments, host suffixes, and prefix collisions.
Call sites converted (real-code only; tests, optional-skills, red-teaming
scripts untouched):
run_agent.py (10 sites):
- AIAgent.__init__ Bedrock branch, ChatGPT/Codex branch (also path check)
- header cascade for openrouter / copilot / kimi / qwen / chatgpt
- interleaved-thinking trigger (openrouter + claude)
- _is_openrouter_url(), _is_qwen_portal()
- is_native_anthropic check
- github-models-vs-copilot detection (3 sites)
- reasoning-capable route gate (nousresearch, vercel, github)
- codex-backend detection in API kwargs build
- fallback api_mode Bedrock detection
agent/auxiliary_client.py (7 sites):
- extra-headers cascades in 4 distinct client-construction paths
(resolve custom, resolve auto, OpenRouter-fallback-to-custom,
_async_client_from_sync, resolve_provider_client explicit-custom,
resolve_auto_with_codex)
- _is_openrouter_client() base_url sniff
agent/usage_pricing.py:
- resolve_billing_route openrouter branch
agent/model_metadata.py:
- _is_openrouter_base_url(), Bedrock context-length lookup
hermes_cli/providers.py:
- determine_api_mode Bedrock heuristic
hermes_cli/runtime_provider.py:
- _is_openrouter_url flag for API-key preference (issues #420, #560)
hermes_cli/doctor.py:
- Kimi User-Agent header for /models probes
tools/delegate_tool.py:
- subagent Codex endpoint detection
trajectory_compressor.py:
- _detect_provider() cascade (8 providers: openrouter, nous, codex, zai,
kimi-coding, arcee, minimax-cn, minimax)
cli.py, gateway/run.py:
- /model-switch cache-enabled hint (openrouter + claude)
Bedrock detection tightened from 'bedrock-runtime in url' to
'hostname starts with bedrock-runtime. AND host is under amazonaws.com'.
ChatGPT/Codex detection tightened from 'chatgpt.com/backend-api/codex in
url' to 'hostname is chatgpt.com AND path contains /backend-api/codex'.
Tests:
- tests/test_base_url_hostname.py extended with a base_url_host_matches
suite (exact match, subdomain, path-segment rejection, host-suffix
rejection, host-prefix rejection, empty-input, case-insensitivity,
trailing dot).
Validation: 651 targeted tests pass (runtime_provider, minimax, bedrock,
gemini, auxiliary, codex_cloudflare, usage_pricing, compressor_fallback,
fallback_model, openai_client_lifecycle, provider_parity, cli_provider_resolution,
delegate, credential_pool, context_compressor, plus the 4 hostname test
modules). 26-assertion E2E call-site verification across 6 modules passes.
2026-04-20 21:17:28 -07:00
|
|
|
"""Targeted tests for ``utils.base_url_hostname`` and ``base_url_host_matches``.
|
fix: extend hostname-match provider detection across remaining call sites
Aslaaen's fix in the original PR covered _detect_api_mode_for_url and the
two openai/xai sites in run_agent.py. This finishes the sweep: the same
substring-match false-positive class (e.g. https://api.openai.com.evil/v1,
https://proxy/api.openai.com/v1, https://api.anthropic.com.example/v1)
existed in eight more call sites, and the hostname helper was duplicated
in two modules.
- utils: add shared base_url_hostname() (single source of truth).
- hermes_cli/runtime_provider, run_agent: drop local duplicates, import
from utils. Reuse the cached AIAgent._base_url_hostname attribute
everywhere it's already populated.
- agent/auxiliary_client: switch codex-wrap auto-detect, max_completion_tokens
gate (auxiliary_max_tokens_param), and custom-endpoint max_tokens kwarg
selection to hostname equality.
- run_agent: native-anthropic check in the Claude-style model branch
and in the AIAgent init provider-auto-detect branch.
- agent/model_metadata: Anthropic /v1/models context-length lookup.
- hermes_cli/providers.determine_api_mode: anthropic / openai URL
heuristics for custom/unknown providers (the /anthropic path-suffix
convention for third-party gateways is preserved).
- tools/delegate_tool: anthropic detection for delegated subagent
runtimes.
- hermes_cli/setup, hermes_cli/tools_config: setup-wizard vision-endpoint
native-OpenAI detection (paired with deduping the repeated check into
a single is_native_openai boolean per branch).
Tests:
- tests/test_base_url_hostname.py covers the helper directly
(path-containing-host, host-suffix, trailing dot, port, case).
- tests/hermes_cli/test_determine_api_mode_hostname.py adds the same
regression class for determine_api_mode, plus a test that the
/anthropic third-party gateway convention still wins.
Also: add asslaenn5@gmail.com → Aslaaen to scripts/release.py AUTHOR_MAP.
2026-04-20 20:58:01 -07:00
|
|
|
|
fix: sweep remaining provider-URL substring checks across codebase
Completes the hostname-hardening sweep — every substring check against a
provider host in live-routing code is now hostname-based. This closes the
same false-positive class for OpenRouter, GitHub Copilot, Kimi, Qwen,
ChatGPT/Codex, Bedrock, GitHub Models, Vercel AI Gateway, Nous, Z.AI,
Moonshot, Arcee, and MiniMax that the original PR closed for OpenAI, xAI,
and Anthropic.
New helper:
- utils.base_url_host_matches(base_url, domain) — safe counterpart to
'domain in base_url'. Accepts hostname equality and subdomain matches;
rejects path segments, host suffixes, and prefix collisions.
Call sites converted (real-code only; tests, optional-skills, red-teaming
scripts untouched):
run_agent.py (10 sites):
- AIAgent.__init__ Bedrock branch, ChatGPT/Codex branch (also path check)
- header cascade for openrouter / copilot / kimi / qwen / chatgpt
- interleaved-thinking trigger (openrouter + claude)
- _is_openrouter_url(), _is_qwen_portal()
- is_native_anthropic check
- github-models-vs-copilot detection (3 sites)
- reasoning-capable route gate (nousresearch, vercel, github)
- codex-backend detection in API kwargs build
- fallback api_mode Bedrock detection
agent/auxiliary_client.py (7 sites):
- extra-headers cascades in 4 distinct client-construction paths
(resolve custom, resolve auto, OpenRouter-fallback-to-custom,
_async_client_from_sync, resolve_provider_client explicit-custom,
resolve_auto_with_codex)
- _is_openrouter_client() base_url sniff
agent/usage_pricing.py:
- resolve_billing_route openrouter branch
agent/model_metadata.py:
- _is_openrouter_base_url(), Bedrock context-length lookup
hermes_cli/providers.py:
- determine_api_mode Bedrock heuristic
hermes_cli/runtime_provider.py:
- _is_openrouter_url flag for API-key preference (issues #420, #560)
hermes_cli/doctor.py:
- Kimi User-Agent header for /models probes
tools/delegate_tool.py:
- subagent Codex endpoint detection
trajectory_compressor.py:
- _detect_provider() cascade (8 providers: openrouter, nous, codex, zai,
kimi-coding, arcee, minimax-cn, minimax)
cli.py, gateway/run.py:
- /model-switch cache-enabled hint (openrouter + claude)
Bedrock detection tightened from 'bedrock-runtime in url' to
'hostname starts with bedrock-runtime. AND host is under amazonaws.com'.
ChatGPT/Codex detection tightened from 'chatgpt.com/backend-api/codex in
url' to 'hostname is chatgpt.com AND path contains /backend-api/codex'.
Tests:
- tests/test_base_url_hostname.py extended with a base_url_host_matches
suite (exact match, subdomain, path-segment rejection, host-suffix
rejection, host-prefix rejection, empty-input, case-insensitivity,
trailing dot).
Validation: 651 targeted tests pass (runtime_provider, minimax, bedrock,
gemini, auxiliary, codex_cloudflare, usage_pricing, compressor_fallback,
fallback_model, openai_client_lifecycle, provider_parity, cli_provider_resolution,
delegate, credential_pool, context_compressor, plus the 4 hostname test
modules). 26-assertion E2E call-site verification across 6 modules passes.
2026-04-20 21:17:28 -07:00
|
|
|
These helpers are used across provider routing, auxiliary client, setup
|
|
|
|
|
wizards, billing routes, and the trajectory compressor to avoid the
|
|
|
|
|
substring-match false-positive class documented in
|
fix: extend hostname-match provider detection across remaining call sites
Aslaaen's fix in the original PR covered _detect_api_mode_for_url and the
two openai/xai sites in run_agent.py. This finishes the sweep: the same
substring-match false-positive class (e.g. https://api.openai.com.evil/v1,
https://proxy/api.openai.com/v1, https://api.anthropic.com.example/v1)
existed in eight more call sites, and the hostname helper was duplicated
in two modules.
- utils: add shared base_url_hostname() (single source of truth).
- hermes_cli/runtime_provider, run_agent: drop local duplicates, import
from utils. Reuse the cached AIAgent._base_url_hostname attribute
everywhere it's already populated.
- agent/auxiliary_client: switch codex-wrap auto-detect, max_completion_tokens
gate (auxiliary_max_tokens_param), and custom-endpoint max_tokens kwarg
selection to hostname equality.
- run_agent: native-anthropic check in the Claude-style model branch
and in the AIAgent init provider-auto-detect branch.
- agent/model_metadata: Anthropic /v1/models context-length lookup.
- hermes_cli/providers.determine_api_mode: anthropic / openai URL
heuristics for custom/unknown providers (the /anthropic path-suffix
convention for third-party gateways is preserved).
- tools/delegate_tool: anthropic detection for delegated subagent
runtimes.
- hermes_cli/setup, hermes_cli/tools_config: setup-wizard vision-endpoint
native-OpenAI detection (paired with deduping the repeated check into
a single is_native_openai boolean per branch).
Tests:
- tests/test_base_url_hostname.py covers the helper directly
(path-containing-host, host-suffix, trailing dot, port, case).
- tests/hermes_cli/test_determine_api_mode_hostname.py adds the same
regression class for determine_api_mode, plus a test that the
/anthropic third-party gateway convention still wins.
Also: add asslaenn5@gmail.com → Aslaaen to scripts/release.py AUTHOR_MAP.
2026-04-20 20:58:01 -07:00
|
|
|
tests/agent/test_direct_provider_url_detection.py.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
fix: sweep remaining provider-URL substring checks across codebase
Completes the hostname-hardening sweep — every substring check against a
provider host in live-routing code is now hostname-based. This closes the
same false-positive class for OpenRouter, GitHub Copilot, Kimi, Qwen,
ChatGPT/Codex, Bedrock, GitHub Models, Vercel AI Gateway, Nous, Z.AI,
Moonshot, Arcee, and MiniMax that the original PR closed for OpenAI, xAI,
and Anthropic.
New helper:
- utils.base_url_host_matches(base_url, domain) — safe counterpart to
'domain in base_url'. Accepts hostname equality and subdomain matches;
rejects path segments, host suffixes, and prefix collisions.
Call sites converted (real-code only; tests, optional-skills, red-teaming
scripts untouched):
run_agent.py (10 sites):
- AIAgent.__init__ Bedrock branch, ChatGPT/Codex branch (also path check)
- header cascade for openrouter / copilot / kimi / qwen / chatgpt
- interleaved-thinking trigger (openrouter + claude)
- _is_openrouter_url(), _is_qwen_portal()
- is_native_anthropic check
- github-models-vs-copilot detection (3 sites)
- reasoning-capable route gate (nousresearch, vercel, github)
- codex-backend detection in API kwargs build
- fallback api_mode Bedrock detection
agent/auxiliary_client.py (7 sites):
- extra-headers cascades in 4 distinct client-construction paths
(resolve custom, resolve auto, OpenRouter-fallback-to-custom,
_async_client_from_sync, resolve_provider_client explicit-custom,
resolve_auto_with_codex)
- _is_openrouter_client() base_url sniff
agent/usage_pricing.py:
- resolve_billing_route openrouter branch
agent/model_metadata.py:
- _is_openrouter_base_url(), Bedrock context-length lookup
hermes_cli/providers.py:
- determine_api_mode Bedrock heuristic
hermes_cli/runtime_provider.py:
- _is_openrouter_url flag for API-key preference (issues #420, #560)
hermes_cli/doctor.py:
- Kimi User-Agent header for /models probes
tools/delegate_tool.py:
- subagent Codex endpoint detection
trajectory_compressor.py:
- _detect_provider() cascade (8 providers: openrouter, nous, codex, zai,
kimi-coding, arcee, minimax-cn, minimax)
cli.py, gateway/run.py:
- /model-switch cache-enabled hint (openrouter + claude)
Bedrock detection tightened from 'bedrock-runtime in url' to
'hostname starts with bedrock-runtime. AND host is under amazonaws.com'.
ChatGPT/Codex detection tightened from 'chatgpt.com/backend-api/codex in
url' to 'hostname is chatgpt.com AND path contains /backend-api/codex'.
Tests:
- tests/test_base_url_hostname.py extended with a base_url_host_matches
suite (exact match, subdomain, path-segment rejection, host-suffix
rejection, host-prefix rejection, empty-input, case-insensitivity,
trailing dot).
Validation: 651 targeted tests pass (runtime_provider, minimax, bedrock,
gemini, auxiliary, codex_cloudflare, usage_pricing, compressor_fallback,
fallback_model, openai_client_lifecycle, provider_parity, cli_provider_resolution,
delegate, credential_pool, context_compressor, plus the 4 hostname test
modules). 26-assertion E2E call-site verification across 6 modules passes.
2026-04-20 21:17:28 -07:00
|
|
|
from utils import base_url_hostname, base_url_host_matches
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─── base_url_hostname ────────────────────────────────────────────────────
|
fix: extend hostname-match provider detection across remaining call sites
Aslaaen's fix in the original PR covered _detect_api_mode_for_url and the
two openai/xai sites in run_agent.py. This finishes the sweep: the same
substring-match false-positive class (e.g. https://api.openai.com.evil/v1,
https://proxy/api.openai.com/v1, https://api.anthropic.com.example/v1)
existed in eight more call sites, and the hostname helper was duplicated
in two modules.
- utils: add shared base_url_hostname() (single source of truth).
- hermes_cli/runtime_provider, run_agent: drop local duplicates, import
from utils. Reuse the cached AIAgent._base_url_hostname attribute
everywhere it's already populated.
- agent/auxiliary_client: switch codex-wrap auto-detect, max_completion_tokens
gate (auxiliary_max_tokens_param), and custom-endpoint max_tokens kwarg
selection to hostname equality.
- run_agent: native-anthropic check in the Claude-style model branch
and in the AIAgent init provider-auto-detect branch.
- agent/model_metadata: Anthropic /v1/models context-length lookup.
- hermes_cli/providers.determine_api_mode: anthropic / openai URL
heuristics for custom/unknown providers (the /anthropic path-suffix
convention for third-party gateways is preserved).
- tools/delegate_tool: anthropic detection for delegated subagent
runtimes.
- hermes_cli/setup, hermes_cli/tools_config: setup-wizard vision-endpoint
native-OpenAI detection (paired with deduping the repeated check into
a single is_native_openai boolean per branch).
Tests:
- tests/test_base_url_hostname.py covers the helper directly
(path-containing-host, host-suffix, trailing dot, port, case).
- tests/hermes_cli/test_determine_api_mode_hostname.py adds the same
regression class for determine_api_mode, plus a test that the
/anthropic third-party gateway convention still wins.
Also: add asslaenn5@gmail.com → Aslaaen to scripts/release.py AUTHOR_MAP.
2026-04-20 20:58:01 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_empty_returns_empty_string():
|
|
|
|
|
assert base_url_hostname("") == ""
|
|
|
|
|
assert base_url_hostname(None) == "" # type: ignore[arg-type]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_plain_host_without_scheme():
|
|
|
|
|
assert base_url_hostname("api.openai.com") == "api.openai.com"
|
|
|
|
|
assert base_url_hostname("api.openai.com/v1") == "api.openai.com"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_https_url_extracts_hostname_only():
|
|
|
|
|
assert base_url_hostname("https://api.openai.com/v1") == "api.openai.com"
|
|
|
|
|
assert base_url_hostname("https://api.x.ai/v1") == "api.x.ai"
|
|
|
|
|
assert base_url_hostname("https://api.anthropic.com") == "api.anthropic.com"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_hostname_case_insensitive():
|
|
|
|
|
assert base_url_hostname("https://API.OpenAI.com/v1") == "api.openai.com"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_trailing_dot_stripped():
|
|
|
|
|
assert base_url_hostname("https://api.openai.com./v1") == "api.openai.com"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_path_containing_provider_host_is_not_the_hostname():
|
|
|
|
|
assert base_url_hostname("https://proxy.example.test/api.openai.com/v1") == "proxy.example.test"
|
|
|
|
|
assert base_url_hostname("https://proxy.example.test/api.anthropic.com/v1") == "proxy.example.test"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_host_suffix_is_not_the_provider():
|
|
|
|
|
assert base_url_hostname("https://api.openai.com.example/v1") == "api.openai.com.example"
|
|
|
|
|
assert base_url_hostname("https://api.x.ai.example/v1") == "api.x.ai.example"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_port_is_ignored():
|
|
|
|
|
assert base_url_hostname("https://api.openai.com:443/v1") == "api.openai.com"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_whitespace_stripped():
|
|
|
|
|
assert base_url_hostname(" https://api.openai.com/v1 ") == "api.openai.com"
|
fix: sweep remaining provider-URL substring checks across codebase
Completes the hostname-hardening sweep — every substring check against a
provider host in live-routing code is now hostname-based. This closes the
same false-positive class for OpenRouter, GitHub Copilot, Kimi, Qwen,
ChatGPT/Codex, Bedrock, GitHub Models, Vercel AI Gateway, Nous, Z.AI,
Moonshot, Arcee, and MiniMax that the original PR closed for OpenAI, xAI,
and Anthropic.
New helper:
- utils.base_url_host_matches(base_url, domain) — safe counterpart to
'domain in base_url'. Accepts hostname equality and subdomain matches;
rejects path segments, host suffixes, and prefix collisions.
Call sites converted (real-code only; tests, optional-skills, red-teaming
scripts untouched):
run_agent.py (10 sites):
- AIAgent.__init__ Bedrock branch, ChatGPT/Codex branch (also path check)
- header cascade for openrouter / copilot / kimi / qwen / chatgpt
- interleaved-thinking trigger (openrouter + claude)
- _is_openrouter_url(), _is_qwen_portal()
- is_native_anthropic check
- github-models-vs-copilot detection (3 sites)
- reasoning-capable route gate (nousresearch, vercel, github)
- codex-backend detection in API kwargs build
- fallback api_mode Bedrock detection
agent/auxiliary_client.py (7 sites):
- extra-headers cascades in 4 distinct client-construction paths
(resolve custom, resolve auto, OpenRouter-fallback-to-custom,
_async_client_from_sync, resolve_provider_client explicit-custom,
resolve_auto_with_codex)
- _is_openrouter_client() base_url sniff
agent/usage_pricing.py:
- resolve_billing_route openrouter branch
agent/model_metadata.py:
- _is_openrouter_base_url(), Bedrock context-length lookup
hermes_cli/providers.py:
- determine_api_mode Bedrock heuristic
hermes_cli/runtime_provider.py:
- _is_openrouter_url flag for API-key preference (issues #420, #560)
hermes_cli/doctor.py:
- Kimi User-Agent header for /models probes
tools/delegate_tool.py:
- subagent Codex endpoint detection
trajectory_compressor.py:
- _detect_provider() cascade (8 providers: openrouter, nous, codex, zai,
kimi-coding, arcee, minimax-cn, minimax)
cli.py, gateway/run.py:
- /model-switch cache-enabled hint (openrouter + claude)
Bedrock detection tightened from 'bedrock-runtime in url' to
'hostname starts with bedrock-runtime. AND host is under amazonaws.com'.
ChatGPT/Codex detection tightened from 'chatgpt.com/backend-api/codex in
url' to 'hostname is chatgpt.com AND path contains /backend-api/codex'.
Tests:
- tests/test_base_url_hostname.py extended with a base_url_host_matches
suite (exact match, subdomain, path-segment rejection, host-suffix
rejection, host-prefix rejection, empty-input, case-insensitivity,
trailing dot).
Validation: 651 targeted tests pass (runtime_provider, minimax, bedrock,
gemini, auxiliary, codex_cloudflare, usage_pricing, compressor_fallback,
fallback_model, openai_client_lifecycle, provider_parity, cli_provider_resolution,
delegate, credential_pool, context_compressor, plus the 4 hostname test
modules). 26-assertion E2E call-site verification across 6 modules passes.
2026-04-20 21:17:28 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─── base_url_host_matches ────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestBaseUrlHostMatchesExact:
|
|
|
|
|
def test_exact_domain_matches(self):
|
|
|
|
|
assert base_url_host_matches("https://openrouter.ai/api/v1", "openrouter.ai") is True
|
|
|
|
|
assert base_url_host_matches("https://moonshot.ai", "moonshot.ai") is True
|
|
|
|
|
|
|
|
|
|
def test_subdomain_matches(self):
|
|
|
|
|
# A subdomain of the registered domain should match — needed for
|
|
|
|
|
# api.moonshot.ai / api.kimi.com / portal.qwen.ai lookups that
|
|
|
|
|
# accept both the bare registrable domain and any subdomain under it.
|
|
|
|
|
assert base_url_host_matches("https://api.moonshot.ai/v1", "moonshot.ai") is True
|
|
|
|
|
assert base_url_host_matches("https://api.kimi.com/v1", "api.kimi.com") is True
|
|
|
|
|
assert base_url_host_matches("https://portal.qwen.ai/v1", "portal.qwen.ai") is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestBaseUrlHostMatchesNegatives:
|
|
|
|
|
"""The reason this helper exists — defend against substring collisions."""
|
|
|
|
|
|
|
|
|
|
def test_path_segment_containing_domain_does_not_match(self):
|
|
|
|
|
assert base_url_host_matches("https://evil.test/moonshot.ai/v1", "moonshot.ai") is False
|
|
|
|
|
assert base_url_host_matches("https://proxy.example.test/openrouter.ai/v1", "openrouter.ai") is False
|
|
|
|
|
assert base_url_host_matches("https://proxy/api.kimi.com/v1", "api.kimi.com") is False
|
|
|
|
|
|
|
|
|
|
def test_host_suffix_does_not_match(self):
|
|
|
|
|
# Attacker-controlled hosts that end with the domain string are not
|
|
|
|
|
# the domain.
|
|
|
|
|
assert base_url_host_matches("https://moonshot.ai.evil/v1", "moonshot.ai") is False
|
|
|
|
|
assert base_url_host_matches("https://openrouter.ai.example/v1", "openrouter.ai") is False
|
|
|
|
|
|
|
|
|
|
def test_host_prefix_does_not_match(self):
|
|
|
|
|
# "fake-openrouter.ai" is not a subdomain of openrouter.ai.
|
|
|
|
|
assert base_url_host_matches("https://fake-openrouter.ai/v1", "openrouter.ai") is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestBaseUrlHostMatchesEdgeCases:
|
|
|
|
|
def test_empty_base_url_returns_false(self):
|
|
|
|
|
assert base_url_host_matches("", "openrouter.ai") is False
|
|
|
|
|
assert base_url_host_matches(None, "openrouter.ai") is False # type: ignore[arg-type]
|
|
|
|
|
|
|
|
|
|
def test_empty_domain_returns_false(self):
|
|
|
|
|
assert base_url_host_matches("https://openrouter.ai/v1", "") is False
|
|
|
|
|
|
|
|
|
|
def test_case_insensitive(self):
|
|
|
|
|
assert base_url_host_matches("https://OpenRouter.AI/v1", "openrouter.ai") is True
|
|
|
|
|
assert base_url_host_matches("https://openrouter.ai/v1", "OPENROUTER.AI") is True
|
|
|
|
|
|
|
|
|
|
def test_trailing_dot_on_domain_stripped(self):
|
|
|
|
|
assert base_url_host_matches("https://openrouter.ai/v1", "openrouter.ai.") is True
|
2026-04-21 06:06:16 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestOllamaUrlHostCheck:
|
|
|
|
|
"""GHSA-76xc-57q6-vm5m — ollama.com was using a raw substring match for
|
|
|
|
|
credential selection (same bug class as GHSA-xf8p-v2cg-h7h5 for OpenRouter).
|
|
|
|
|
These tests lock in that the base_url_host_matches fix correctly rejects
|
|
|
|
|
the same attack vectors for Ollama.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def test_ollama_com_path_injection_rejected(self):
|
|
|
|
|
"""http://evil.test/ollama.com/v1 — ollama.com appears in the path,
|
|
|
|
|
not the host. Must not be treated as Ollama Cloud."""
|
|
|
|
|
assert base_url_host_matches(
|
|
|
|
|
"http://127.0.0.1:9000/ollama.com/v1", "ollama.com"
|
|
|
|
|
) is False
|
|
|
|
|
|
|
|
|
|
def test_ollama_com_subdomain_lookalike_rejected(self):
|
|
|
|
|
"""ollama.com.attacker.test is a separate host, not ollama.com."""
|
|
|
|
|
assert base_url_host_matches(
|
|
|
|
|
"http://ollama.com.attacker.test:9000/v1", "ollama.com"
|
|
|
|
|
) is False
|
|
|
|
|
|
|
|
|
|
def test_ollama_com_localtest_me_rejected(self):
|
|
|
|
|
"""ollama.com.localtest.me resolves to 127.0.0.1 via localtest.me
|
|
|
|
|
but its true hostname is localtest.me, not ollama.com."""
|
|
|
|
|
assert base_url_host_matches(
|
|
|
|
|
"http://ollama.com.localtest.me:9000/v1", "ollama.com"
|
|
|
|
|
) is False
|
|
|
|
|
|
|
|
|
|
def test_ollama_ai_is_not_ollama_com(self):
|
|
|
|
|
"""Different TLD. ollama.ai is not ollama.com."""
|
|
|
|
|
assert base_url_host_matches(
|
|
|
|
|
"https://ollama.ai/v1", "ollama.com"
|
|
|
|
|
) is False
|
|
|
|
|
|
|
|
|
|
def test_localhost_ollama_port_is_not_ollama_com(self):
|
|
|
|
|
"""http://localhost:11434/v1 is a local Ollama install, but its
|
|
|
|
|
hostname is localhost, so OLLAMA_API_KEY (an ollama.com-only secret)
|
|
|
|
|
must not be sent."""
|
|
|
|
|
assert base_url_host_matches(
|
|
|
|
|
"http://localhost:11434/v1", "ollama.com"
|
|
|
|
|
) is False
|
|
|
|
|
|
|
|
|
|
def test_genuine_ollama_com_matches(self):
|
|
|
|
|
assert base_url_host_matches(
|
|
|
|
|
"https://ollama.com/api/generate", "ollama.com"
|
|
|
|
|
) is True
|
|
|
|
|
|
|
|
|
|
def test_ollama_com_subdomain_matches(self):
|
|
|
|
|
assert base_url_host_matches(
|
|
|
|
|
"https://api.ollama.com/v1", "ollama.com"
|
|
|
|
|
) is True
|