Skip to content

fix(providers): bound model-catalog response reads (#54735) - #54765

Closed
iceTTTT wants to merge 1 commit into
NousResearch:mainfrom
iceTTTT:fix/bound-model-catalog-reads
Closed

fix(providers): bound model-catalog response reads (#54735)#54765
iceTTTT wants to merge 1 commit into
NousResearch:mainfrom
iceTTTT:fix/bound-model-catalog-reads

Conversation

@iceTTTT

@iceTTTT iceTTTT commented Jun 29, 2026

Copy link
Copy Markdown

Summary

ProviderProfile.fetch_models() fetched the provider model catalog and parsed it with json.loads(resp.read().decode())no cap on the response body. A malicious, compromised, or misconfigured model endpoint could return an arbitrarily large body and make Hermes allocate it while opening the model picker, probing providers, or refreshing live model lists.

The call already treats fetch failures as non-fatal and falls back to the static model list, so oversized responses should fail closed the same way. This PR makes them do exactly that.

Change

Add a shared _read_json_capped() helper in providers/base.py with a two-layer guard:

  1. Reject up front if the declared Content-Length exceeds the cap.
  2. Bounded read of at most cap + 1 bytes and reject if the body actually ran past the cap (covers a missing or lying Content-Length on a streamed response).

On oversize it raises ValueError, which the existing except Exception: return None path turns into the same fall-back to the static model list — the exact fail-closed behavior the issue asks for.

Applied to both unbounded sites so the whole bug class is closed:

  • providers/base.py — generic ProviderProfile.fetch_models() (the openrouter plugin reuses this via super().fetch_models(...)).
  • plugins/model-providers/anthropic/__init__.py — the sibling AnthropicProfile.fetch_models() override, which had the identical unbounded resp.read() shape. The issue's Scope note flagged provider-specific fetches for a separate sweep; this one is structurally identical to the base path, so it's folded in here.

Cap is 16 MiB (_MAX_MODELS_RESPONSE_BYTES) — generous for real catalogs (tens-to-hundreds of KB) while bounding a malicious payload.

Tests

New tests/providers/test_fetch_models_bounded.py asserts the behavior contract (not a model-name snapshot) against a real local HTTPServer:

  • oversized Content-LengthNone
  • oversized streamed body (no honest Content-Length) → None
  • normal small body → parsed model list (no false reject)
  • helper-level checks that the read is bounded (read(cap + 1), never an unbounded read())

Verified with scripts/run_tests.sh (new file + regression on test_fetch_models_base_url, test_provider_profiles, test_plugin_discovery, anthropic picker/persistence) and ruff.

Relation to #42930

#42930 (approved) bounds the read in providers/base.py only. This PR additionally covers the sibling AnthropicProfile.fetch_models() unbounded read, closing the bug class rather than the single reported site.

Closes #54735

ProviderProfile.fetch_models() parsed the model-catalog response with
json.loads(resp.read().decode()) and no size cap. A malicious, compromised,
or misconfigured model endpoint could return an arbitrarily large body and
make Hermes allocate it while opening the model picker, probing providers, or
refreshing live model lists.

Add a shared _read_json_capped() helper with a two-layer guard:
  1. reject up front if the declared Content-Length exceeds the cap;
  2. read at most cap+1 bytes and reject if the body actually ran past the cap
     (covers a missing or lying Content-Length on a streamed response).

On oversize it raises ValueError, which the existing
`except Exception: return None` path turns into a fall-back to the static
model list — the exact fail-closed behavior the issue asks for.

Applied to both unbounded sites: the generic ProviderProfile.fetch_models()
(which the openrouter plugin reuses via super()) and the sibling
AnthropicProfile.fetch_models() override, so the whole bug class is closed.

Cap is 16 MiB (_MAX_MODELS_RESPONSE_BYTES) — generous for real catalogs
(tens-to-hundreds of KB) while bounding malicious payloads.

Tests assert the behavior contract (oversize Content-Length -> None,
oversize streamed body -> None, normal small body -> list) against a real
local HTTPServer, plus helper-level checks that the read is bounded
(read(cap+1), never an unbounded read()).

Closes NousResearch#54735

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@iceTTTT

iceTTTT commented Jun 29, 2026

Copy link
Copy Markdown
Author

Closing in favor of #42930, which already bounds the read in providers/base.py and is approved. The anthropic sibling read can be folded into that PR or a focused follow-up. Apologies for the duplicate.

@iceTTTT iceTTTT closed this Jun 29, 2026
@iceTTTT
iceTTTT deleted the fix/bound-model-catalog-reads branch June 29, 2026 09:17
@alt-glitch alt-glitch added type/security Security vulnerability or hardening P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint provider/anthropic Anthropic native Messages API area/config Config system, migrations, profiles labels Jun 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists provider/anthropic Anthropic native Messages API type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bound provider model catalog response reads

2 participants