Commit Graph
292 Commits
Author SHA1 Message Date
a6494e6fe3 perf: eliminate per-request callback scanning on proxy hot path (#27858)
- Introduce `_CallbackCapabilities` dataclass and `ProxyLogging._callback_capabilities()` static method that inspects `litellm.callbacks` once and caches capability flags keyed on (list length, member ids); invalidates automatically when the callback list mutates without per-request iteration overhead
- Replace O(n) `litellm.callbacks` walks in `async_pre_call_hook`, `during_call_hook`, `async_post_call_streaming_iterator_hook`, `async_post_call_streaming_hook`, and `post_call_response_headers_hook` with fast-path exits when no relevant callbacks are registered
- Add `needs_iterator_wrap()` and `needs_per_chunk_streaming_hook()` instance methods to decouple iterator-level wrapping from per-chunk hook execution; avoids `get_response_string` materialization per chunk when no guardrail or chunk-hook callback is active
- Introduce `_fast_serialize_simple_model_response_stream()` using `orjson` for common single-choice text streaming chunks, bypassing the full Pydantic serializer; falls back to `model_dump_json` for tool calls, logprobs, usage, and provider-specific fields
- Add early-return in `_restamp_streaming_chunk_model` when downstream model already matches the requested model, avoiding unnecessary string comparisons on every chunk
- Fix stale zero-cost cache bug in `_is_model_cost_zero`: move the per-router `_zero_cost_cache` dict onto the `Router` instance and clear it in `_invalidate_model_group_info_cache` so in-place pricing updates via `upsert_deployment` immediately resume budget enforcement
- Add `scripts/benchmark_chat_completions_perf.py`: standalone async benchmarking tool with a mock OpenAI provider, LiteLLM proxy process management, non-streaming RPS, streaming TTFT, and full-stream latency measurements with repeat/median run support
- Add comprehensive unit tests covering capability detection, cache invalidation, fast-path correctness, zero-cost cache regression, and the no-callback streaming fast path

Co-authored-by: Yassin Kortam <yassinkortam@g.ucla.edu>
2026-05-14 09:28:31 -07:00
Krrish DholakiaGitHubClaude Sonnet 4.6veria-ai[bot] <224490171+veria-ai[bot]@users.noreply.github.com>
8bbc61e03c fix: harden /key/update authorization checks (#27878)
* fix: patch Host-header auth bypass in get_request_route

Starlette reconstructs request.url from the Host header. A malformed
Host like `localhost/?x=1` causes Starlette to build the full URL as
`http://localhost/?x=1/health`, which url-parses to path="/". Since "/"
is in LiteLLMRoutes.public_routes, all protected routes became reachable
without authentication.

Fix: read scope["path"] (set by uvicorn from the HTTP request line,
not derivable from headers) instead of request.url.path. Sub-path
deployments are handled via scope["app_root_path"] / scope["root_path"],
mirroring Starlette's own base_url construction logic.

Affected variants confirmed fixed:
  Host: localhost/?x=1
  Host: localhost:4000/?x=1
  Host: localhost/#test
  Host: localhost:4000/#test

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* style: reduce comments in route fix

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix: block credential fields in RAG ingest vector_store options

Credential fields (vertex_credentials, aws_access_key_id, api_key, etc.)
in ingest_options.vector_store are now rejected at the API boundary with
a 400 error. Credentials must be configured server-side.

Previously any authenticated user could supply a vertex_credentials dict
with type=external_account pointing credential_source.file at an
arbitrary path (e.g. /proc/1/environ) and token_url at an
attacker-controlled server. google-auth's identity_pool.Credentials
refresh() would read the file and POST its contents to the attacker.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix: block /key/update self-escalation by assigned users

Non-admin users who were assigned a key (created_by != caller) could
update any non-budget field — models, rpm_limit, guardrails, etc. —
without admin authorization, allowing privilege self-escalation.

Gate: only the key creator (created_by == caller) may edit their own
key without admin check; budget changes always require admin regardless
of creator status. All other callers must pass _check_key_admin_access.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix: block user-controlled api_base in RAG ingest vector_store options

A user-supplied api_base in ingest_options.vector_store caused the server
to forward its configured provider credentials (Gemini, OpenAI) to an
attacker-controlled endpoint via SSRF.

Add api_base to the blocked credential params set alongside api_key and
the existing credential fields.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix: restrict /utils/transform_request to PROXY_ADMIN and apply body safety check

Any authenticated internal_user could POST arbitrary provider config
(aws_sts_endpoint, api_base, etc.) to /utils/transform_request and have
the server forward its credentials to an attacker-controlled endpoint.

- Gate the endpoint on PROXY_ADMIN role (403 for all other roles)
- Call is_request_body_safe() to reject banned params even for admins
- Convert ValueError from safety check to HTTP 400

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix: apply banned-param check to /utils/transform_request

Without is_request_body_safe(), any authenticated user could pass
aws_sts_endpoint, api_base, or aws_web_identity_token to
/utils/transform_request and have the server forward its configured
provider credentials to an attacker-controlled endpoint during SDK
credential resolution.

Applies the same banned-param blocklist already used by LLM endpoints.
Endpoint remains accessible to all authenticated users.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix: block SSRF via api_base in /prompts/test dotprompt YAML frontmatter

Any frontmatter key not in ["model","input","output"] flowed into
optional_params and was merged into the LLM call data dict, bypassing
is_request_body_safe. An attacker with any bearer key could set
api_base in YAML to redirect the outbound LLM request — including the
provider API key — to an attacker-controlled host.

Fix: call is_request_body_safe on the constructed data dict after
optional_params are merged, before invoking ProxyBaseLLMRequestProcessing.
ValueError from the banned-param check is surfaced as HTTP 400.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* Update litellm/proxy/rag_endpoints/endpoints.py

Co-authored-by: veria-ai[bot] <224490171+veria-ai[bot]@users.noreply.github.com>

* fix: coerce nested config strings before banned-param check

_NESTED_CONFIG_KEYS descent used isinstance(nested, dict) which silently
skipped litellm_embedding_config when delivered as a JSON string via
multipart/form-data. Banned params (api_base, aws_sts_endpoint, etc.)
nested inside the stringified value were invisible to is_request_body_safe.

_NESTED_METADATA_KEYS already used _coerce_metadata_to_dict which parses
JSON strings before checking. Apply the same coercion to _NESTED_CONFIG_KEYS.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix: replace substring match with prefix match in is_llm_api_route

mapped_pass_through_routes used `_llm_passthrough_route in route` (substring)
so any admin-only path whose URL contained a provider name (openai, anthropic,
azure, bedrock, etc.) was misclassified as an LLM API route and bypassed the
admin gate in non_proxy_admin_allowed_routes_check.

Confirmed live: non-admin key could GET /credentials/by_name/openai (read
masked provider API key) and DELETE /credentials/openai (delete credential).

Fix: use exact match or startswith(prefix + "/") — the same pattern used
everywhere else in RouteChecks — so only routes that actually start with a
passthrough prefix are allowed through.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix: stabilize PR #27878 test failures

- key_management_endpoints: extend can_skip_admin_check to team keys so
  team members with /key/update permission can update non-budget fields.
  can_team_member_execute_key_management_endpoint already validates team
  membership + permission and raises if unauthorized; reaching the admin
  check on a team key means the caller was authorized.

- test: set created_by on mock key in
  test_update_key_non_budget_fields_allowed_for_internal_user so
  caller_is_creator resolves correctly (MagicMock default ≠ user_id).

- auth_utils.get_request_route: guard against non-dict request.scope
  (e.g. MagicMock in unit tests) to prevent a MagicMock leaking into
  UserAPIKeyAuth.request_route and failing Pydantic validation.

- ci: assign test_multipart_bypass_repro.py to the proxy-runtime shard
  in test-unit-proxy-db.yml to satisfy the shard-coverage check.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix(lint): add explicit str() cast in get_request_route for MyPy

scope.get() returns Any|None which MyPy cannot coerce to str implicitly.
Wrap both scope.get() calls in str() to satisfy the type checker.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix: guard bare-/ root_path strip + make total_spend migration idempotent

auth_utils.get_request_route: when Starlette sets scope["app_root_path"]
to "/" (e.g. behind some middleware), the old stripping logic would
remove the leading slash from every path ("/team/new" → "team/new"),
breaking route matching and causing auth to misclassify protected routes.
Skip stripping when root_path is bare "/".

migration: add IF NOT EXISTS to total_spend ALTER TABLE so the migration
is safe to replay when a prior partial run already created the column.
Without this guard, prisma migrate deploy fails on CI DBs that were
partially migrated, causing all subsequent DB operations (including
/team/new) to 500.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix: require creator still owns key for personal-key bypass in /key/update

caller_is_creator now requires both created_by == caller AND user_id ==
caller. Previously checking only created_by let a demoted admin who
originally created a key for another user continue editing non-budget
fields on it after reassignment, bypassing _check_key_admin_access.

Adds regression test: creator whose key was reassigned is blocked (403).

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix: extract auth checks to fix PLR0915 + broaden max_budget assertion

internal_user_endpoints._update_single_user_helper exceeded 50 statements
(PLR0915). Extract authorization checks into _check_user_update_authz helper
to bring statement count under the limit.

test_validate_max_budget: assert "negative" (substring of both the local
"cannot be negative" and the CI "non-negative finite number" messages) so
the test is stable regardless of which exact wording the function uses.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: veria-ai[bot] <224490171+veria-ai[bot]@users.noreply.github.com>
2026-05-14 04:16:04 +00:00
yuneng-jiangandGitHub 0c4982042a Merge pull request #27892 from BerriAI/worktree-fix-mcp-byok-oauth
Worktree fix mcp byok oauth
2026-05-13 21:06:25 -07:00
user 626b768d25 chore(tests): drop redundant membership check; trim test comment
``test_azure_ad_token_is_in_banned_list`` only asserted tuple
membership of a name the parametrized test already exercises end-to-end
through ``is_request_body_safe``. Removed.

Tightened the admin-opt-in test comment.
2026-05-14 03:39:15 +00:00
user 5e56022553 chore(proxy): coerce stringified nested-config containers before descent
``_NESTED_CONFIG_KEYS`` descent used ``isinstance(nested, dict)``, so a
caller sending ``extra_body`` as a JSON-encoded string instead of an
object (the same shape multipart/form-data clients use for
``litellm_metadata``) skipped the banned-key check entirely. Switched to
``_coerce_metadata_to_dict`` so the JSON-string path is parsed before
descent — mirrors the existing handling on ``_NESTED_METADATA_KEYS``.
2026-05-14 03:37:14 +00:00
user 2519ac161e chore(proxy): cover extra_body + azure_ad_token in banned-params check
``extra_body`` is the OpenAI-SDK passthrough container. Provider
modules read provider-auth fields out of it directly (Azure's
``extra_body.azure_ad_token``, Bedrock's
``extra_body.aws_web_identity_token``, etc.) without re-validating, so
the boundary check has to walk it the same way it walks
``litellm_embedding_config``. Adding it to ``_NESTED_CONFIG_KEYS``
extends single-level banned-key descent into the container — top-level
admin opt-ins (``allow_client_side_credentials`` /
``configurable_clientside_auth_params``) still apply.

``azure_ad_token`` was not in ``_BANNED_REQUEST_BODY_PARAMS`` despite
being the bearer-token field the Azure transformer resolves through
``get_secret`` (same shape as ``aws_web_identity_token`` on the
Bedrock STS path). Added so it can't be supplied per-request without
an admin opt-in.
2026-05-14 03:29:16 +00:00
Krrish DholakiaandClaude Sonnet 4.6 b95130eb32 fix: block client-side pricing injection via request body
Authenticated clients could supply CustomPricingLiteLLMParams fields
(input_cost_per_token, output_cost_per_token, etc.) in the request body.
These were forwarded to register_model() in main.py, permanently mutating
the shared global litellm.model_cost dict for all users on the instance.

Adds all CustomPricingLiteLLMParams fields to _BANNED_REQUEST_BODY_PARAMS
so is_request_body_safe() rejects them before they reach completion().
New pricing fields added to CustomPricingLiteLLMParams are auto-covered.

Admin opt-in via allow_client_side_credentials or
configurable_clientside_auth_params still works as before.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-13 17:05:36 -07:00
12e59c8798 Fix internal tag usage scoping (#27315)
* Scope internal tag usage to own keys

Co-authored-by: ishaan-berri <ishaan-berri@users.noreply.github.com>

* Add internal tag usage unowned key regression test

Co-authored-by: ishaan-berri <ishaan-berri@users.noreply.github.com>

* Handle empty internal tag usage scopes safely

Co-authored-by: ishaan-berri <ishaan-berri@users.noreply.github.com>

* Add tag activity database guard

Co-authored-by: ishaan-berri <ishaan-berri@users.noreply.github.com>

---------

Co-authored-by: oss-agent-shin <279349115+oss-agent-shin@users.noreply.github.com>
Co-authored-by: ishaan-berri <ishaan-berri@users.noreply.github.com>
2026-05-11 10:44:50 -07:00
Sameer KankuteandGitHub 9ed99037d2 Merge pull request #27422 from BerriAI/shin_agent_oss_staging_05_07_2026
[litellm-agent] Staging → litellm_internal_staging (5/7/2026)
2026-05-11 11:58:05 +05:30
Sameer KankuteandCursor 055bdc3507 fix(auth): harden JWT routing wildcard iss and merge list team_id claims
Reject fnmatch wildcards on non-scope claims when the claim string contains
whitespace so malformed iss values cannot match patterns like trusted.*.

Merge every entry when team_id_jwt_field resolves to a list instead of
keeping only the first element.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 11:35:56 +05:30
Sameer KankuteandGitHub ce17e9490f Merge branch 'litellm_internal_staging' into litellm_agent_oss_staging_05_06_2026 2026-05-11 09:07:08 +05:30
d67dfca1e1 Fix proxy auth status code tests (#27555)
* Fix proxy auth status code tests

Co-authored-by: ishaan-berri <ishaan-berri@users.noreply.github.com>

* Update user model access status expectation

Co-authored-by: ishaan-berri <ishaan-berri@users.noreply.github.com>

---------

Co-authored-by: oss-agent-shin <279349115+oss-agent-shin@users.noreply.github.com>
Co-authored-by: ishaan-berri <ishaan-berri@users.noreply.github.com>
2026-05-09 14:47:48 -07:00
ae67cecc22 Allow team admins to test model connections (#27487)
Co-authored-by: oss-agent-shin <279349115+oss-agent-shin@users.noreply.github.com>
Co-authored-by: ishaan-berri <ishaan-berri@users.noreply.github.com>
2026-05-08 15:30:41 -07:00
milan-berriandGitHub bac03ac3f1 feat(auth): add scope and wildcard support for JWT routing overrides (#26325)
Squash-merged by litellm-agent from milan-berri's PR.
2026-05-07 21:34:49 +00:00
oss-pr-review-agent-shin[bot]andGitHub 158b0c28c0 [litellm-agent] Staging → litellm_internal_staging (5/7/2026) (#27375)
Squash-merged by litellm-agent from oss-pr-review-agent-shin[bot]'s PR.
2026-05-07 21:29:47 +00:00
db8198faba [Fix] Allow non-admin compliance path reads (#27234)
* allow non-admin roles on /compliance/* read routes

* Restrict compliance routes to internal users

---------

Co-authored-by: Michael Riad Zaky <michaelr@Mac.localdomain>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-05-07 14:07:23 -05:00
169c436684 Fix/member access group team (#27317)
* fix(auth): pass team_id in member-level model access check

_check_team_member_model_access calls _can_object_call_model without
team_id, so access groups defined via model_info.access_groups cannot
resolve for team-scoped DB models (their internal router name is
model_name_<team>_<uuid>, not the public name). The team-level check
already passes team_id; this mirrors that.

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

* test(auth): add tests for member-level access group resolution with team_id

Eight tests covering _can_object_call_model and
_check_team_member_model_access with team-scoped DB models:

- access group resolves when team_id is passed
- access group fails without team_id (pre-fix behavior)
- literal model name still works with team_id (no regression)
- denied model still denied with team_id
- second model in group also reachable
- end-to-end member access via access group (mocked membership)
- end-to-end member denied for model not in allowed list
- no-override member inherits team-level check

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-06 12:05:22 -07:00
c92a08a307 Fix team member budget enforcement without user row (#27273)
* Fix team member budget enforcement without user row

Co-authored-by: ishaan-berri <ishaan-berri@users.noreply.github.com>

* Clarify regenerated key budget repro

Co-authored-by: ishaan-berri <ishaan-berri@users.noreply.github.com>

---------

Co-authored-by: oss-agent-shin <279349115+oss-agent-shin@users.noreply.github.com>
Co-authored-by: ishaan-berri <ishaan-berri@users.noreply.github.com>
2026-05-06 11:42:29 -07:00
oss-pr-review-agent-shin[bot]andGitHub abf0381878 Merge PR #27234 into agent staging branch 2026-05-06 01:00:23 +00:00
yuneng-jiangandGitHub 9a338e1b6b [Test] Tests: Stop parametrizing API keys into pytest test IDs (#27249)
Several tests parametrized over (model, api_key, ...) tuples or raw
token strings, causing pytest to embed those values in the test ID
and print them in CI logs. Refactored each affected test to keep the
same coverage without putting key material into parametrize.

- audio_tests/test_audio_speech.py: split env-var keys into separate
  azure/openai test functions sharing a helper; sync_mode parametrize
  preserved.
- audio_tests/test_whisper.py: split into openai_whisper /
  azure_whisper functions sharing a helper; response_format parametrize
  preserved.
- local_testing/test_embedding.py: single-case parametrize inlined.
- proxy_unit_tests/test_user_api_key_auth.py: 5 header parametrize
  cases split into 5 named tests sharing an _assert helper.
- proxy_unit_tests/test_proxy_utils.py: 4 api_key_value cases split
  into 4 named tests.
- test_litellm/proxy/auth/test_user_api_key_auth.py: 5 key-prefix
  cases (Bearer / Basic / lowercase bearer / raw / AWS SigV4) split
  into 5 named tests.

Verified: black clean; 14 refactored unit tests pass; pytest collects
audio/embedding tests with safe IDs (no key material in test IDs).
2026-05-05 17:21:18 -07:00
Michael Riad Zaky 2993e45ad1 allow non-admin roles on /compliance/* read routes 2026-05-05 14:21:52 -07:00
yuneng-jiangandGitHub be5f217aaf Merge pull request #26861 from BerriAI/litellm_fix_scim_virtual_key_deactivation
fix(scim): revoke virtual keys when SCIM deprovisions a user
2026-05-04 19:03:55 -07:00
yuneng-jiangandGitHub 42cd9493e9 Merge pull request #27071 from stuxf/fix/strip-pricing-fields
chore(proxy): drop client-supplied pricing fields from request bodies
2026-05-04 18:08:41 -07:00
yuneng-jiangandGitHub de7175d6ab Merge pull request #26912 from stuxf/codex/auth-sensitive-routes
chore(proxy): guard sensitive public endpoints
2026-05-04 17:04:10 -07:00
user abcf204d38 fix(proxy): include request-blocked callback params in auth bans 2026-05-04 16:54:04 -07:00
yuneng-jiangandGitHub e4ac46b5d1 Merge pull request #27081 from stuxf/fix/strip-callback-fields
chore(proxy): close callback-config and observability-credential side channels
2026-05-04 15:45:42 -07:00
Michael-RZ-BerriandGitHub 1a17c438b6 Merge pull request #27133 from BerriAI/litellm_zeroBudgetTreatedAsNoCap
[Fix] Treat 0 team_member_budget as no cap
2026-05-04 15:05:18 -07:00
yuneng-jiangandGitHub 6f5678bcd8 Merge pull request #27007 from stuxf/fix/admin-viewer-write-route-blocklist
fix(auth): block missing write routes for proxy admin viewers
2026-05-04 14:45:14 -07:00
Michael Riad Zaky 28bf4647ef Treat 0 team_member_budget as no cap 2026-05-04 14:45:13 -07:00
userandClaude Opus 4.7 01323e8903 fix(auth): per-param allow must continue, not return early
A pre-existing logic bug in ``_check_banned_params``: when the
deployment-level ``configurable_clientside_auth_params`` permitted one
banned field, the loop ``return``-ed on the first match instead of
``continue``-ing, so any other banned param later in the same body or
metadata dict was never checked. This PR's metadata walk multiplies the
surface where that bypass matters — a body pairing an allowed
``api_base`` with an observability credential like ``langfuse_host``
would silently pass.

Proxy-wide ``allow_client_side_credentials`` keeps ``return`` (it's a
global opt-in for every banned param). The per-param branch becomes
``continue`` so only the one explicitly-permitted field is skipped.

Adds a regression test that exercises the api_base + langfuse_host pair.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:18:17 +00:00
userandClaude Opus 4.7 37a22acf6f chore(proxy): close callback-config and observability-credential side channels
Two related gaps in the proxy's request bouncer:

1. ``is_request_body_safe`` (auth_utils.py) walked the request-body root
   and the ``litellm_embedding_config`` nested dict, but not ``metadata``
   or ``litellm_metadata``. The same fields it bans at root — Langfuse /
   Langsmith / Arize / PostHog / Braintrust / Phoenix / W&B Weave / GCS /
   Humanloop / Lunary credentials and routing — were silently accepted
   when the caller put them inside metadata, retargeting observability
   callbacks to a caller-controlled host with caller-supplied creds.
   Walk both metadata containers (and parse the JSON-string form sent via
   multipart / ``extra_body``) through the same banned-params helper, so
   the existing ``allow_client_side_credentials`` opt-in covers both
   paths consistently.

2. The banned-params list was hand-maintained and lagged the canonical
   ``_supported_callback_params`` allow-list in
   ``initialize_dynamic_callback_params``. Derive the observability bans
   from that allow-list (minus a small ``_SAFE_CLIENT_CALLBACK_PARAMS``
   set for informational fields like ``langfuse_prompt_version`` and
   ``langsmith_sampling_rate``) so future integrations are covered
   automatically; ``_EXTRA_BANNED_OBSERVABILITY_PARAMS`` carries the
   handful of fields integrations read but the allow-list hasn't caught
   up to. A guard test fails CI if a new entry is added to
   ``_supported_callback_params`` without an explicit safe-list decision.

Separately in ``litellm_pre_call_utils.py``: add ``callbacks``,
``service_callback``, ``logger_fn``, and ``litellm_disabled_callbacks``
to ``_UNTRUSTED_ROOT_CONTROL_FIELDS``. The first three are appended to
worker-wide ``litellm.{input,success,failure,_async_*,service}_callback``
lists / ``litellm.user_logger_fn`` from inside ``function_setup`` — one
request poisons every subsequent caller in that worker. The last is the
inverse primitive: the legitimate path reads it from key/team metadata,
the request-body version silently disables admin-configured audit /
observability for the call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:06:29 +00:00
mateo-berri 456cb495de Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_fix_scim_virtual_key_deactivation 2026-05-01 20:29:19 -07:00
yuneng-jiangandGitHub c3f7158b2b Merge pull request #27008 from stuxf/fix/jwt-audience-and-issuer-verification
fix(auth): support JWT issuer verification + warn when unscoped
2026-05-01 19:58:52 -07:00
shin-berriandGitHub 38ddcdabdb Merge pull request #27032 from BerriAI/litellm_yj_may1_2
[Infra] Merge dev branch
2026-05-01 19:39:42 -07:00
user 3fd0a2d761 Merge remote-tracking branch 'origin/litellm_internal_staging' into codex/auth-sensitive-routes
# Conflicts:
#	litellm/proxy/health_endpoints/_health_endpoints.py
#	tests/test_litellm/proxy/auth/test_user_api_key_auth.py
2026-05-01 19:08:06 -07:00
user bef28aa789 chore(proxy): keep public AI hub unauthenticated 2026-05-01 19:07:21 -07:00
Krrish DholakiaandGitHub 684174ca58 Merge branch 'litellm_internal_staging' into fix/admin-viewer-write-route-blocklist 2026-05-01 18:55:29 -07:00
yuneng-jiangandGitHub 5614469f22 Merge pull request #26825 from stuxf/fix/oauth2-proxy-header-forgery
chore(auth): require trusted proxy for header identity auth
2026-05-01 18:47:58 -07:00
Yuneng Jiang b484c51a1c [Fix] Proxy: Repair Merge Fallout In Router-Override Fallback Auth
Conflict resolution for #26968 dropped the `Iterator` typing import
(NameError at module load), left a dead `fallback_models = cast(...)`
block, and the new tests called `_enforce_key_and_fallback_model_access`
without the now-required `request` kwarg.
2026-05-01 17:48:51 -07:00
yuneng-jiangandGitHub 8fc31a7b3a Merge branch 'litellm_yj_may1_2' into chore/router-override-trust 2026-05-01 17:26:04 -07:00
yuneng-jiangandGitHub 8ed6c0cdea Merge pull request #26846 from BerriAI/litellm_/pensive-bartik-e24048
[Fix] RBAC: Restore Admin Viewer Read Parity for Logs + Settings Pages
2026-05-01 16:36:36 -07:00
Yuneng Jiang 6499fa76de [Fix] RBAC: Drop management_routes Write Fallback for Admin Viewer
Greptile P1: the unsafe-method branch of `_check_proxy_admin_viewer_access`
ended with a blanket `if route in management_routes: return`. That set is a
mix of reads (info/list — handled via the safe-method GET branch above) and
writes. The fallback let Admin Viewer POST to write endpoints not enumerated
in `_ADMIN_VIEWER_BLOCKED_WRITE_ROUTES`, including:
  - /team/block, /team/unblock, /team/permissions_update
  - /jwt/key/mapping/{new,update,delete}
  - /key/bulk_update
  - /key/{key_id}/reset_spend

Remove the fallback. The two remaining allow sets (admin_viewer_routes and
global_spend_tracking_routes) are both read-only, so removal does not affect
the legitimate POST-as-read cases (e.g. /spend/calculate, which is in
spend_tracking_routes ⊂ admin_viewer_routes).

Tests:
  - 8 new parametrized cases pinning each previously-leaking management write
    endpoint to 403 on POST for PROXY_ADMIN_VIEW_ONLY.
2026-05-01 16:15:21 -07:00
Yuneng Jiang c78144ccf0 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/pensive-bartik-e24048
# Conflicts:
#	ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx
2026-05-01 16:04:09 -07:00
yuneng-jiangandGitHub c2cea58567 Merge branch 'litellm_yj_may1' into codex/budget-race-enforcement 2026-05-01 14:32:18 -07:00
yuneng-jiangandGitHub 9e501edece Merge branch 'litellm_yj_may1' into codex/file-endpoint-model-auth 2026-05-01 14:22:18 -07:00
yuneng-jiangandGitHub f34a2752f6 Merge pull request #26996 from stuxf/chore/ssrf-polling-and-nested-config
chore(security): close two unaddressed SSRF cases
2026-05-01 14:16:39 -07:00
userandClaude Opus 4.7 e55401e39c fix(auth): support JWT issuer verification, scope-warning when unscoped
When JWT auth is enabled but `JWT_AUDIENCE` is unset, `auth_jwt`
disabled audience verification entirely. Tokens minted by any other
application that shared the same IdP signing keys (Azure AD, Okta,
etc.) were accepted as long as their signature checked out, even
though their `aud` and `iss` claims pointed at unrelated apps. The
proxy then fell into the no-team / no-user branch where access checks
default-allow.

This change:

1. Adds support for the `JWT_ISSUER` env var. When set, PyJWT verifies
   the token's `iss` claim — turning on the same defense for tokens
   that share an audience but come from a different IdP tenant.
2. Refactors the duplicated `jwt.decode` calls (RSA/EC/OKP path and
   x509 path) into a single `_build_decode_kwargs` helper that
   computes audience, issuer, and the corresponding `verify_*` opt-outs
   once per call.
3. Logs a single startup-time warning when JWT auth is enabled but
   neither `JWT_AUDIENCE` nor `JWT_ISSUER` is configured, so operators
   running the insecure default see a flag in their logs without
   getting spammed per-request.

Default behavior (no env vars) is preserved for backward compatibility.
Setting `JWT_AUDIENCE` and/or `JWT_ISSUER` opts into the verification.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 21:10:19 +00:00
userandClaude Opus 4.7 c27951b53b fix(auth): block missing write routes for proxy admin viewers
`_check_proxy_admin_viewer_access` enumerates write routes a
PROXY_ADMIN_VIEW_ONLY caller may not invoke, then falls through to
"allow" for any management route not listed. Several write endpoints
were never added to the blocklist, so a viewer could:

- block or unblock any team via `/team/block` / `/team/unblock`
- mutate team permissions via `/team/permissions_update` and
  `/team/permissions_bulk_update`
- create, update, or delete JWT key mappings via
  `/jwt/key/mapping/{new,update,delete}`
- bulk-edit keys via `/key/bulk_update`
- reset key spend via the path-parameterized `/key/{id}/reset_spend`

Hoist the blocklist into a module-level frozenset and a tuple of
suffix patterns so it's clear what to extend when a new write route
is added, and pull the existing key write routes from the
`KeyManagementRoutes` enum so the two stay in sync. Adds parametrized
tests over the newly-blocked routes plus baseline coverage for routes
that should remain allowed (info / list / daily-activity reads).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 21:06:16 +00:00
userandClaude Opus 4.7 90fd791e0d fix(security): close P1 recursion-DoS + P2 hostname leak in SSRF fixes
Greptile follow-ups on the prior commit:

- (P1) ``is_request_body_safe`` recursed into ``litellm_embedding_config``
  with no depth bound, so a request body 1000 levels deep could exhaust
  Python's call stack and surface a 500 ``RecursionError``. Refactored
  the check to be iterative (single-level descent into a fixed list of
  nested-config keys) and extracted the per-dict banned-param scan into
  a helper that's shared between the root and the nested call sites.
  Also fixes the ``recursive_detector`` CI job that was triggered by
  the recursive-by-name pattern.

- (P2) ``assert_same_origin`` error messages identified the mismatching
  component but echoed the ``expected`` host and the candidate
  hostname back to the caller. In the SSRF threat model the caller is
  the attacker, so reflecting that information was a secondary leak of
  operator infrastructure. Messages now identify only *which*
  component mismatched (scheme / host / port) without naming names.

- (P2) ``_NESTED_CONFIG_KEYS`` was defined after the function that used
  it. Hoisted the constant (and the new ``_BANNED_REQUEST_BODY_PARAMS``
  tuple) above the function for readability.

Adds a 1000-level-deep nested config test that asserts no
``RecursionError`` and a hostname-leak test that asserts no operator
host appears in the rejection message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 18:52:55 +00:00
userandClaude Opus 4.7 0d4875dec9 chore(security): close two unaddressed SSRF cases
Two SSRF findings were OPEN with no in-flight fix; both are closed
now using narrow defenses that key off existing trust boundaries.

VERIA-6 (Milvus ``litellm_embedding_config``):
``is_request_body_safe`` already blocks ``api_base`` / ``api_key`` /
``langfuse_host`` / ``s3_endpoint_url`` / etc. at the *root* of the
request body, gated by an admin opt-in (``allow_client_side_credentials``
or per-deployment ``configurable_clientside_auth_params``). The bug is
that the Milvus vector-store transformer unpacks
``litellm_embedding_config`` into ``litellm.embedding(**embedding_config)``,
so a caller can smuggle the same banned params in via nesting and bypass
the check. Fix: ``is_request_body_safe`` now recurses into a known list
of nested-config dicts (``litellm_embedding_config`` for now) and applies
the same banned-param check with the same admin opt-in. Admin-side
vector-store config flows through ``litellm_params`` rather than the
request body, so it's unaffected.

VERIA-51 (polling URLs returned by upstream APIs):
Azure DALL-E 2, Azure Document Intelligence, and Black Forest Labs
all blindly fetched a polling URL returned by the upstream and
attached the operator's API key to the request. A compromised upstream
or a future API contract change could redirect credentials anywhere.
New ``url_utils.assert_same_origin(candidate, expected)`` helper checks
scheme, host (case-insensitive), and port (with default-port
normalization). Applied at all five polling sites: Azure DALL-E
sync+async, Azure DI sync+async, BFL image generation sync+async, BFL
image edit sync+async. Cross-origin polling URLs now raise rather than
forward credentials. The Azure DALL-E ``Expected 'status' in response``
exception no longer reflects the raw response body — that path turned
Blind SSRF into Full-Read SSRF for the limited window before the
origin check fully closed it.

Tests: 7 ``assert_same_origin`` unit tests, 6 ``is_request_body_safe``
nested-config tests, 5 polling-site rejection tests + 1 same-origin
sanity check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 18:43:47 +00:00