Skip to content

OPENAI_API_KEY / OPENROUTER_API_KEY leak to non-OpenAI custom-provider base_urls (cross-provider credential leak) #28660

@pmos69

Description

@pmos69

What I'm seeing

When model.provider is custom in config.yaml and model.base_url points at a non-OpenRouter, non-OpenAI commercial endpoint (DeepSeek, Groq, Mistral, etc.), the gateway sends the user's OPENAI_API_KEY (or OPENROUTER_API_KEY) to that endpoint.

Manifests as a 401 with a masked key tail that doesn't match anything the user thinks they configured:

Error code: 401 - {'error': {'message': 'Authentication Fails, Your api key: ****ired is invalid', ...}}

The ****ired is the last 4 chars of OPENAI_API_KEY (or whatever else trailing-matched).

Root cause

hermes_cli/runtime_provider.py::_resolve_openrouter_runtime, the fallback chain for custom endpoints:

# When hitting a custom endpoint (e.g. Z.ai, local LLM), prefer
# OPENAI_API_KEY so the OpenRouter key doesn't leak to an unrelated
# provider (issues #420, #560).
_is_ollama_url = base_url_host_matches(base_url, "ollama.com")
api_key_candidates = [
    explicit_api_key,
    (cfg_api_key if use_config_base_url else ""),
    (os.getenv("OLLAMA_API_KEY") if _is_ollama_url else ""),
    os.getenv("OPENAI_API_KEY"),         # ← runs for *any* non-openrouter URL
    os.getenv("OPENROUTER_API_KEY"),     # ← same
]

The Ollama path correctly gates on host (_is_ollama_url), but OPENAI_API_KEY and OPENROUTER_API_KEY are added unconditionally. So provider="custom" + base_url="https://api.deepseek.com/v1" ends up sending an OpenAI key to DeepSeek.

Symmetrical to (and predates) the OpenRouter-leak fix in the comment block above — same class of bug, just a different scapegoat key.

Reproduction

# ~/.hermes/config.yaml
model:
  provider: "custom"
  default: "deepseek-chat"
  base_url: "https://api.deepseek.com/v1"

providers: {}
# ~/.hermes/.env
OPENAI_API_KEY=sk-something-from-an-old-experiment
# DEEPSEEK_API_KEY intentionally unset

Send any chat → 401 from DeepSeek complaining about an sk-… key it doesn't recognize.

Suggested fix

Gate OPENAI_API_KEY / OPENROUTER_API_KEY on the resolved host, the same way OLLAMA_API_KEY is already gated. The principle: an env var that's named for a specific provider should only feed an endpoint pointed at that provider:

_is_openrouter_url = base_url_host_matches(base_url, "openrouter.ai")
_is_openai_url    = base_url_host_matches(base_url, "openai.com")
_is_ollama_url    = base_url_host_matches(base_url, "ollama.com")

api_key_candidates = [
    explicit_api_key,
    (cfg_api_key if use_config_base_url else ""),
    (os.getenv("OLLAMA_API_KEY")     if _is_ollama_url     else ""),
    (os.getenv("OPENAI_API_KEY")     if _is_openai_url     else ""),
    (os.getenv("OPENROUTER_API_KEY") if _is_openrouter_url else ""),
]

For non-OpenAI / non-OpenRouter custom hosts, the chain then ends with cfg_api_key (matching the existing Ollama precedent) — effective_provider == "custom" and not api_key already falls back to "no-key-required" (line 718–719), so local LLMs without a key continue to work.

Bonus: it would be nice to also read a <NAME>_API_KEY derived from the host (e.g. api.deepseek.comDEEPSEEK_API_KEY) as an extra candidate. Not strictly required to close this bug, but it's what users intuitively expect.

Environment

Reproduced on hermes-agent 0.14.x.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P0Critical — data loss, security, crash looparea/authAuthentication, OAuth, credential poolscomp/agentCore agent loop, run_agent.py, prompt buildertype/securitySecurity vulnerability or hardening

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions