fix: restrict provider URL detection to exact hostname matches (salvage #13229) - #13302
Conversation
🚨 CRITICAL Supply Chain Risk DetectedThis PR contains a pattern that has been used in real supply chain attacks. A maintainer must review the flagged code carefully before merging. 🚨 CRITICAL: Install-hook file added or modifiedThese files can execute code during package installation or interpreter startup. Files: Scanner only fires on high-signal indicators: .pth files, base64+exec/eval combos, subprocess with encoded commands, or install-hook files. Low-signal warnings were removed intentionally — if you're seeing this comment, the finding is worth inspecting. |
…ites 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.
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.
00913c9 to
5c0e695
Compare
🚨 CRITICAL Supply Chain Risk DetectedThis PR contains a pattern that has been used in real supply chain attacks. A maintainer must review the flagged code carefully before merging. 🚨 CRITICAL: Install-hook file added or modifiedThese files can execute code during package installation or interpreter startup. Files: Scanner only fires on high-signal indicators: .pth files, base64+exec/eval combos, subprocess with encoded commands, or install-hook files. Low-signal warnings were removed intentionally — if you're seeing this comment, the finding is worth inspecting. |
…known-provider branch NousResearch#13302 swept the substring->hostname URL check across the codebase but missed determine_api_mode()'s known-provider branch (get_provider(provider) is not None), which still does `"api.openai.com" in url_lower`. The sibling unknown-provider branch right below already uses `base_url_hostname(...) == "api.openai.com"`. A known provider behind a path-style proxy (".../api.openai.com/v1") or a lookalike host ("api.openai.com.evil/v1") is therefore given the wrong api_mode (codex_responses / anthropic_messages instead of the transport default) -> hard 400/404 on the wire after a /model switch. Reachable from switch_model() (agent_runtime_helpers.py) and model_switch.py with a real provider. Match on the parsed hostname, mirroring the sibling branch's checks (base_url_hostname already imported -> zero new deps); base_url_hostname normalises scheme-less hosts so those still route correctly. The /anthropic path-suffix convention is preserved. Adds TestKnownProviderHostHardening (known-provider branch incl. scheme-less + path-style + lookalike cases).
Salvages #13229 (@Aslaaen) and sweeps the same substring → hostname class across the entire codebase, not just OpenAI/xAI/Anthropic.
Summary
Every live-routing site that used
"provider.host" in urlto detect a known provider now parses the hostname. Substring matching treats attacker- or proxy-controlled paths/hosts likehttps://evil/openrouter.ai/v1orhttps://api.openai.com.example/v1as native endpoints, leading to wrong api_mode, wrong auth keys, wrong provider-specific headers (codex Cloudflare tokens, copilot tokens), or wrong billing routes.Commits (in order)
_detect_api_mode_for_url, AIAgent xAI branch,_is_direct_openai_url,is_xai_responses.utils.base_url_hostname(single source of truth).utils.base_url_host_matches(base_url, domain)and sweeps every remaining provider-host substring check in live-routing code. 26 call sites across 14 files.Providers now hardened
OpenAI, xAI, Anthropic (original PR + commit 2), OpenRouter, GitHub Copilot, Kimi, Qwen (portal), ChatGPT/Codex, Bedrock, GitHub Models, Vercel AI Gateway, Nous Research, Z.AI, Moonshot, Arcee, MiniMax.
What was vulnerable before
hermes_cli/runtime_provider._is_openrouter_urlhttps://evil/openrouter.ai/v1would getOPENROUTER_API_KEYattached (issue #420, #560 class)cli.pyAPI-key selectorrun_agent.pyheader cascade × 2agent/auxiliary_client.pyheader/client cascades × 7agent/usage_pricing.resolve_billing_routetrajectory_compressor._detect_providerhermes_cli/providers.determine_api_modetools/delegate_toolhermes_cli/doctor/modelsprobe Kimi headercli.py+gateway/run.py/model switch hintsNew helpers in
utilsbase_url_hostname(url)— returns the lowercased hostname, empty if absent. Handles bare hosts, full URLs, trailing dots, ports, whitespace.base_url_host_matches(url, domain)— safe counterpart todomain in url. True on exact hostname and subdomain matches; False on path segments, host suffixes, host prefixes, empty inputs.Bedrock detection tightened from
"bedrock-runtime" in urltohostname starts with bedrock-runtime. AND host is under amazonaws.com. ChatGPT/Codex detection tightened from"chatgpt.com/backend-api/codex" in urltohostname is chatgpt.com AND path contains /backend-api/codex.Tests
tests/test_base_url_hostname.py— 18 tests covering both helpers: path-contains-host, host-suffix, host-prefix (fake-openrouter.ai), trailing dot, port, case, empty inputs, subdomain-matches.tests/hermes_cli/test_determine_api_mode_hostname.py— 6 tests for OpenAI/Anthropic determine_api_mode plus third-party/anthropicpath-suffix convention.Validation
main)py_compileacross 14 touched source filesPre-existing failures confirmed on main (unrelated):
test_compatible_custom_providers_prefers_api,TestGeminiModelCatalog::test_provider_models_exist,TestHuggingFaceModels::test_model_metadata_has_context_lengths.Closes #13229.