mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
security(runtime_provider): close OLLAMA_API_KEY substring-leak sweep miss (#13522)
Two call sites still used a raw substring check to identify ollama.com:
hermes_cli/runtime_provider.py:496:
_is_ollama_url = "ollama.com" in base_url.lower()
run_agent.py:6127:
if fb_base_url_hint and "ollama.com" in fb_base_url_hint.lower() ...
Same bug class as GHSA-xf8p-v2cg-h7h5 (OpenRouter substring leak), which
was fixed in commit dbb7e00e via base_url_host_matches() across the
codebase. The earlier sweep missed these two Ollama sites. Self-discovered
during April 2026 security-advisory triage; filed as GHSA-76xc-57q6-vm5m.
Impact is narrow — requires a user with OLLAMA_API_KEY configured AND a
custom base_url whose path or look-alike host contains 'ollama.com'.
Users on default provider flows are unaffected. Filed as a draft advisory
to use the private-fork flow; not CVE-worthy on its own.
Fix is mechanical: replace substring check with base_url_host_matches
at both sites. Same helper the rest of the codebase uses.
Tests: 67 -> 71 passing. 7 new host-matcher cases in
tests/test_base_url_hostname.py (path injection, lookalike host,
localtest.me subdomain, ollama.ai TLD confusion, localhost, genuine
ollama.com, api.ollama.com subdomain) + 4 call-site tests in
tests/hermes_cli/test_runtime_provider_resolution.py verifying
OLLAMA_API_KEY is selected only when base_url actually targets
ollama.com.
Fixes GHSA-76xc-57q6-vm5m
This commit is contained in:
parent
4cc5065f63
commit
7fc1e91811
4 changed files with 148 additions and 4 deletions
|
|
@ -1412,3 +1412,90 @@ def test_named_custom_runtime_no_model_when_absent(monkeypatch):
|
|||
|
||||
resolved = rp.resolve_runtime_provider(requested="my-server")
|
||||
assert "model" not in resolved
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GHSA-76xc-57q6-vm5m — Ollama URL substring leak
|
||||
#
|
||||
# Same bug class as the previously-fixed GHSA-xf8p-v2cg-h7h5 (OpenRouter).
|
||||
# _resolve_openrouter_runtime's custom-endpoint branch selects OLLAMA_API_KEY
|
||||
# when the base_url "looks like" ollama.com. Previous implementation used
|
||||
# raw substring match; a custom base_url whose PATH or look-alike host
|
||||
# merely contained "ollama.com" leaked OLLAMA_API_KEY to that endpoint.
|
||||
# Fix: use base_url_host_matches (same helper as the OpenRouter sweep).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestOllamaUrlSubstringLeak:
|
||||
"""Call-site regression tests for the fix in _resolve_openrouter_runtime."""
|
||||
|
||||
def _make_cfg(self, base_url):
|
||||
return {"base_url": base_url, "api_key": "", "provider": "custom"}
|
||||
|
||||
def test_ollama_key_not_leaked_to_path_injection(self, monkeypatch):
|
||||
"""http://127.0.0.1:9000/ollama.com/v1 — attacker endpoint with
|
||||
ollama.com in PATH. Must resolve to OPENAI_API_KEY, not OLLAMA_API_KEY."""
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "oa-secret")
|
||||
monkeypatch.setenv("OPENROUTER_API_KEY", "or-secret")
|
||||
monkeypatch.setenv("OLLAMA_API_KEY", "ol-SECRET-should-not-leak")
|
||||
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "custom")
|
||||
monkeypatch.setattr(rp, "_get_model_config", lambda: self._make_cfg(
|
||||
"http://127.0.0.1:9000/ollama.com/v1"
|
||||
))
|
||||
monkeypatch.setattr(rp, "load_pool", lambda provider: None)
|
||||
monkeypatch.setattr(rp, "_try_resolve_from_custom_pool", lambda *a, **k: None)
|
||||
|
||||
resolved = rp.resolve_runtime_provider(requested="custom")
|
||||
|
||||
assert "ol-SECRET" not in resolved["api_key"], (
|
||||
"OLLAMA_API_KEY must not be sent to an endpoint whose "
|
||||
"hostname is not ollama.com (GHSA-76xc-57q6-vm5m)"
|
||||
)
|
||||
assert resolved["api_key"] == "oa-secret"
|
||||
|
||||
def test_ollama_key_not_leaked_to_lookalike_host(self, monkeypatch):
|
||||
"""ollama.com.attacker.test — look-alike host. OLLAMA_API_KEY
|
||||
must not be sent."""
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "oa-secret")
|
||||
monkeypatch.setenv("OLLAMA_API_KEY", "ol-SECRET-should-not-leak")
|
||||
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "custom")
|
||||
monkeypatch.setattr(rp, "_get_model_config", lambda: self._make_cfg(
|
||||
"http://ollama.com.attacker.test:9000/v1"
|
||||
))
|
||||
monkeypatch.setattr(rp, "load_pool", lambda provider: None)
|
||||
monkeypatch.setattr(rp, "_try_resolve_from_custom_pool", lambda *a, **k: None)
|
||||
|
||||
resolved = rp.resolve_runtime_provider(requested="custom")
|
||||
|
||||
assert "ol-SECRET" not in resolved["api_key"]
|
||||
assert resolved["api_key"] == "oa-secret"
|
||||
|
||||
def test_ollama_key_sent_to_genuine_ollama_com(self, monkeypatch):
|
||||
"""https://ollama.com/v1 — legit Ollama Cloud. OLLAMA_API_KEY
|
||||
should be used."""
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "oa-secret")
|
||||
monkeypatch.setenv("OLLAMA_API_KEY", "ol-legit-key")
|
||||
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "custom")
|
||||
monkeypatch.setattr(rp, "_get_model_config", lambda: self._make_cfg(
|
||||
"https://ollama.com/v1"
|
||||
))
|
||||
monkeypatch.setattr(rp, "load_pool", lambda provider: None)
|
||||
monkeypatch.setattr(rp, "_try_resolve_from_custom_pool", lambda *a, **k: None)
|
||||
|
||||
resolved = rp.resolve_runtime_provider(requested="custom")
|
||||
|
||||
assert resolved["api_key"] == "ol-legit-key"
|
||||
|
||||
def test_ollama_key_sent_to_ollama_subdomain(self, monkeypatch):
|
||||
"""https://api.ollama.com/v1 — legit subdomain."""
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "oa-secret")
|
||||
monkeypatch.setenv("OLLAMA_API_KEY", "ol-legit-key")
|
||||
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "custom")
|
||||
monkeypatch.setattr(rp, "_get_model_config", lambda: self._make_cfg(
|
||||
"https://api.ollama.com/v1"
|
||||
))
|
||||
monkeypatch.setattr(rp, "load_pool", lambda provider: None)
|
||||
monkeypatch.setattr(rp, "_try_resolve_from_custom_pool", lambda *a, **k: None)
|
||||
|
||||
resolved = rp.resolve_runtime_provider(requested="custom")
|
||||
|
||||
assert resolved["api_key"] == "ol-legit-key"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue