Commit graph

4669 commits

Author SHA1 Message Date
user
926de696a1 tighten budget counter cache recovery 2026-04-29 20:51:07 -07:00
Sameer Kankute
50ef2d51a2
Merge pull request #26855 from BerriAI/litellm_internal_staging
merge main
2026-04-30 09:09:45 +05:30
Sameer Kankute
d3891e6eae
Merge pull request #26719 from BerriAI/litellm_fix-bedrock-stream-interrupt-spend-da73
fix(passthrough): track spend for interrupted Bedrock streams
2026-04-30 09:09:08 +05:30
user
72674b06b5 test(team): add /team/permissions_update regression for available-team bypass
Greptile P2: the bypass removal in update_team_member_permissions had
no dedicated regression test.  Adds an integration-style test that
posts to /team/permissions_update as a non-admin caller while
``_is_available_team`` is mocked True, and asserts a 403 — pinning
the bypass-removal against future regressions in the same way the
new member-add unit tests pin the self-join enforcement.
2026-04-30 03:37:41 +00:00
user
5a619cf879 tighten budget spend admission 2026-04-29 20:30:09 -07:00
user
e4c4d9832f chore(team): close authz bypass via the available-team check
Two paths previously treated ``_is_available_team`` as a blanket
authorization bypass — the function was meant to let standard users
self-join a public team but was wired into the broader admin gate
without bounding the action being performed.  Three concrete
exposures resulted:

1. ``/team/member_add``: the bypass let an unprivileged caller add
   themselves as a Team Admin, or add an arbitrary other ``user_id``
   into the team.
2. ``/team/permissions_update``: the same bypass let any authenticated
   user overwrite a team's ``team_member_permissions`` array, mutating
   the access policy for every member.
3. (Read endpoint ``/team/permissions_list`` is unchanged — it leaks
   read-only policy state to non-members but is out of scope of the
   advisory's recommendation; tracking separately.)

This commit:

- Splits ``_validate_team_member_add_permissions`` into early-return
  admin checks followed by an available-team self-join branch that
  enforces ``member.user_id == caller.user_id`` AND
  ``member.role == "user"`` for every member entry in the request.
  The bulk shape (``member: List[Member]``) is checked the same way,
  so a list with one valid self-entry plus one ``role=admin`` entry
  is rejected.  Email-only members are rejected on the self-join
  path: matching by ``user_id`` is the only safe primitive at
  pre-validation time (resolving email→user_id earlier would let
  unauthenticated callers probe user existence).

- Removes the ``_is_available_team`` clause from
  ``update_team_member_permissions`` entirely.  Only proxy / team /
  org admins can update permission policies.

Tests:

- Update the two existing ``_validate_team_member_add_permissions``
  unit tests to pass the new ``data`` argument.
- Add six regression tests covering the privesc shape (role=admin),
  the cross-user-injection shape (other user_id), the no-caller-uid
  fail-closed case, the email-only rejection, and the bulk shape.
- ``test_team_endpoints.py`` 133/133 pass.
2026-04-30 03:23:46 +00:00
Sameer Kankute
26cb28efc5
Merge pull request #26814 from sruthi-sixt-26/fix/bedrock-batch-retrieve
fix(proxy/batches): forward model to retrieve_batch for bedrock
2026-04-30 08:53:23 +05:30
user
1ebb192cbe fix(proxy): catch callback metadata validation errors 2026-04-29 20:13:34 -07:00
user
22c01adeb2 chore(proxy): ignore invalid callback metadata rows 2026-04-29 20:05:35 -07:00
Sameer Kankute
6588564a88
Merge pull request #26691 from BerriAI/litellm_team_search_credentials_metadata
feat(proxy): add team-level search provider credentials
2026-04-30 08:35:17 +05:30
user
1bb04cdc72 fix(mcp): close redirect bypass + empty-getaddrinfo gap on SSRF guard
Three follow-ups to the OAuth-discovery SSRF guard:

1. Greptile P1 (redirect bypass): the validated origin could return a
   3xx whose ``Location`` points at an internal address, and httpx
   would follow without re-checking the new target.  Pass
   ``follow_redirects=False`` to both gated httpx GETs.  Spec-compliant
   OAuth/OIDC metadata endpoints serve the JSON directly, so this
   doesn't affect legitimate providers.

2. Greptile P2 (empty getaddrinfo): POSIX doesn't strictly forbid an
   empty success-list from ``getaddrinfo``.  Add an explicit
   ``if not infos: return False`` so the guard fails closed instead of
   falling through to ``return True``.

3. Mypy: ``info[4][0]`` is typed ``str | int``; narrow at the
   boundary with an ``isinstance`` check (fail-closed if non-str).

Adds two regression tests verifying ``follow_redirects=False`` is
passed at both gated fetch sites, and one verifying the empty-list
case rejects the URL.
2026-04-30 02:59:50 +00:00
user
4d056a1f01 test(proxy): cover callback env metadata rejection 2026-04-29 19:56:37 -07:00
ryan-crabbe-berri
b1b98c3fc7
Merge pull request #26821 from BerriAI/litellm_fix-guardrail-auth-check
fix(proxy/auth): tighten guardrail modification permission check
2026-04-29 19:48:34 -07:00
ryan-crabbe-berri
7c8e95888a
Merge pull request #26831 from BerriAI/litellm_fix-passthrough-batch-identity
fix(proxy): inherit caller identity in passthrough batch managed-object
2026-04-29 19:48:26 -07:00
yuneng-jiang
383bf12001
Merge pull request #26809 from BerriAI/litellm_teamMemberNullBudgetFallback
[Fix] Team member null budget fallback
2026-04-29 19:44:41 -07:00
user
df62dd8768 chore(mcp): SSRF guard on OAuth metadata discovery follow-up fetches
The OAuth discovery code in mcp_server_manager followed two
attacker-influenceable URLs without validation: the
``resource_metadata`` URL parsed out of a ``WWW-Authenticate``
challenge, and the ``authorization_servers[0]`` field of the
PRM JSON returned by the resource server.  A malicious MCP server
could point those at a cloud-instance-metadata service, an internal
admin panel, or a loopback debug endpoint and the proxy would issue
a blind GET on its behalf.

Add ``_is_safe_metadata_url(url, server_url)`` and gate both follow-
up fetch sites on it.  A URL is allowed when:

  - it shares scheme + host + port with ``server_url`` (well-known
    endpoints constructed from the admin's URL, and PRM published at
    the resource server itself per RFC 9728 §3.3), or
  - it resolves to publicly-routable IPs only (covers federated
    authorization servers — Azure Entra, Google, Okta, GitHub —
    hosted cross-origin from the resource server).

URLs that resolve to private / loopback / link-local / cloud-metadata
addresses, or that don't resolve at all, are rejected.  ``http`` and
``https`` are the only schemes accepted.  The IP block list is
provided by the existing ``_is_blocked_ip`` helper from
``litellm_core_utils.url_utils`` so the policy stays consistent with
the rest of the proxy.

The guard does not protect against active DNS rebinding between
this resolution and the subsequent httpx GET — the same-authority
pin remains the primary mitigation; the IP check is defence in
depth.  The surface only triggers on config load / add-server, not
per request, so the synchronous ``getaddrinfo`` is acceptable.

Threads ``server_url`` through ``_fetch_oauth_metadata_from_resource``,
``_fetch_authorization_server_metadata``, and
``_fetch_single_authorization_server_metadata``.  Existing tests for
those helpers updated for the new signature; new
``TestOAuthDiscoverySSRFGuard`` covers same-authority allow,
private-IP rejection across IPv4 and IPv6, multi-A-record dual-
stack rejection, unresolvable hosts, non-http schemes, and
end-to-end "no network call when guard denies".
2026-04-30 02:43:30 +00:00
Cursor Agent
8759413312
refactor: trim explanatory comments from streaming-flush fix
Strip module-level docstrings and per-test/per-block prose from the
LIT-2642 fix and tests. Keep one short comment in each streaming site
that flags the GeneratorExit-vs-Exception subtlety, since that's the
non-obvious reason the flush lives in finally rather than after the loop.

Pure cleanup; no behavior change. All 12 regression tests still pass.

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>
2026-04-30 02:39:28 +00:00
Cursor Agent
1ef034bff6
fix(passthrough): flush spend tracking on interrupted Bedrock streams
When a client disconnects mid-stream from a Bedrock pass-through endpoint,
Starlette calls aclose() on the async generator, raising GeneratorExit
(a BaseException, not Exception) at the suspended yield. The previous
`except Exception` blocks in _async_streaming/_sync_streaming
(litellm/passthrough/main.py) and PassThroughStreamingHandler.chunk_processor
did not catch GeneratorExit, so the post-loop flush that hands collected
raw bytes to async_flush_passthrough_collected_chunks /
_route_streaming_logging_to_handler never ran. All per-chunk usage data
was silently dropped, undercounting spend for interrupted Bedrock invoke
and converse streams.

Move the flush into a finally block in all three sites and guard with a
`flush_scheduled` flag so the success path still flushes exactly once.
Also pull raise_for_status() out of the chunk-collection try block in
_async_streaming so 4xx/5xx responses still raise and don't enter the
flush path with zero bytes (preserving the behavior tested by
test_async_streaming_error_propagation.py).

Add regression coverage:
- test_async_streaming_flushes_on_client_disconnect
- test_async_streaming_flushes_on_upstream_exception_with_partial_data
- test_sync_streaming_flushes_on_early_close
- test_chunk_processor_logs_on_client_disconnect
plus baseline tests for normal completion and the 4xx no-flush path.

Fixes LIT-2642.

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>
2026-04-30 02:39:28 +00:00
yuneng-jiang
2e561bd04e
Merge pull request #26463 from stuxf/fix/mcp-routing-auth
fix(mcp): tighten public-route detection and OAuth2 fallback gating
2026-04-29 19:31:00 -07:00
yuneng-jiang
c6c546ba86
Merge pull request #26484 from stuxf/fix/master-key-pass-the-hash
chore(auth): substitute alias for master key on UserAPIKeyAuth
2026-04-29 19:30:09 -07:00
yuneng-jiang
00d71f586c
Merge pull request #26489 from stuxf/fix/vector-store-list-cred-leak
chore(vector-stores): redact credentials in list/info/update responses; gate update by per-store access
2026-04-29 19:28:32 -07:00
Yuneng Jiang
f81fbdabe6
[Fix] RBAC: Add /invitation/info to admin_viewer_routes
Greptile review caught that the /invitation/info handler relaxation was
dead code: the route_checks layer rejects admin viewers before the handler
runs because /invitation/info was never added to admin_viewer_routes.

Add /invitation/info to admin_viewer_routes and extend the route-level
parametrized test to cover it.

The handler-level integration test passed previously because
`app.dependency_overrides[user_api_key_auth]` bypasses route_checks; this
new route-level test exercises the layer that production traffic hits.
2026-04-29 19:27:25 -07:00
Yuneng Jiang
cfda5e17ac
Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/pensive-bartik-e24048
# Conflicts:
#	ui/litellm-dashboard/src/components/leftnav.tsx
2026-04-29 19:25:20 -07:00
Yuneng Jiang
b1ee9c4fc5
fix(rbac): restore admin-viewer read parity for Logs page + settings reads
Admin Viewer (proxy_admin_viewer) was being blocked from endpoints it should
be able to read. Most visibly the UI Logs page rendered empty because every
filter and detail call (/spend/logs/ui, /spend/logs/ui/{id},
/spend/logs/session/ui, /customer/list) was rejected at the route_checks
layer even though the underlying handlers permit admin-viewer.

Backend:
- Extend admin_viewer_routes to include spend_tracking_routes,
  /customer/{list,info}, /spend/logs/* detail routes, callback / config /
  budget / alerting reads, and model cost map status/source.
- Replace bare `user_role != PROXY_ADMIN` checks in read-only handlers
  (/budget/list, /budget/settings, /alerting/settings, /invitation/info,
  /config/field/info, /config/list, /schedule/model_cost_map_reload/status,
  /model/cost_map/source) with `_user_has_admin_view()`.

UI:
- Add `rolesAllowedToViewWriteScopedPages` (rolesWithWriteAccess + Admin
  Viewer) and use it for the "Models + Endpoints" and "Agents" sidebar
  items so admin viewers see them read-only. Playground stays gated by
  rolesWithWriteAccess (cost-incurring).
- Hide Add / Edit / Delete buttons in the LLM Credentials panel for
  non-proxy-admin viewers.

Tests:
- 31 parametrized route_checks cases for the Logs + settings endpoints,
  with internal-user negative coverage to ensure the gate isn't widened.
- 9 handler-level integration tests (FastAPI TestClient) verifying
  admin viewer is no longer blocked at the handler layer.
- New leftnav cases asserting Playground hidden / Models + Agents / Logs
  visible to Admin Viewer.
- New roles + credentials test cases for the UI write-gate.
2026-04-29 19:21:41 -07:00
Yuneng Jiang
be3d27a0b8
[Test] Proxy: Add /config/update critical-path tests to test_proxy_server.py
Some checks failed
Unit Tests: Caching (Redis) / caching-redis (push) Has been cancelled
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Has been cancelled
Unit Tests: Security / security (push) Has been cancelled
Unit Tests: Proxy DB Operations / auth-checks (push) Has been cancelled
Unit Tests: Proxy DB Operations / budgets (push) Has been cancelled
Unit Tests: Proxy DB Operations / custom-logging (push) Has been cancelled
Unit Tests: Proxy DB Operations / db-and-spend (push) Has been cancelled
Unit Tests: Proxy DB Operations / endpoints-and-responses (push) Has been cancelled
Unit Tests: Proxy DB Operations / guardrails-hooks (push) Has been cancelled
Unit Tests: Proxy DB Operations / jwt-and-keys (push) Has been cancelled
Unit Tests: Proxy DB Operations / key-generation (push) Has been cancelled
Unit Tests: Proxy DB Operations / logging-misc (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-runtime (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-server-core (push) Has been cancelled
Unit Tests: Proxy DB Operations / schema-migration (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-utils (push) Has been cancelled
Companion to the previous commit which deleted the symbol-named
tests/test_litellm/proxy/management_endpoints/test_update_config_endpoint.py.
Adds the 5 critical-path tests in their proper home — the test file
that mirrors the source file (proxy_server.py).

The two commits are one logical change; they were split because git
add aborted on a stale path argument.
2026-04-29 19:18:31 -07:00
Yuneng Jiang
6d16b822ef
[Test] Proxy: Move /config/update tests into test_proxy_server.py and trim to critical paths
Following standard pytest convention (test_<source_filename>.py), tests
for code in litellm/proxy/proxy_server.py belong in
tests/test_litellm/proxy/test_proxy_server.py — not a separate
symbol-named file. Delete tests/test_litellm/proxy/management_endpoints/
test_update_config_endpoint.py and re-home the coverage there.

Also condense from 11 tests to 5 critical paths — the behaviors that
broke or changed in the rewrite of update_config:

1. Targeted writes — only the sent section is persisted; other rows
   left byte-identical (the original bug fix)
2. store_model_in_db chicken-and-egg — endpoint accepts requests when
   the global flag is False, so it can be flipped to True
3. Environment variables encrypted before DB write
4. litellm_settings request-wins merge for non-callback keys
5. success_callback normalizes existing mixed-case entries before
   union dedup

All 5 use FastAPI's TestClient against the real /config/update route
(not direct function calls) so they exercise the same path as a real
admin UI request.

Dropped: redundant first-write / mixed-case-fresh tests, generic auth
+ no-DB error-path tests, alert_to_webhook_url side-effect test, and
the router_settings merge test (overlaps with litellm_settings test).
2026-04-29 19:18:02 -07:00
user
20b937dcbe chore(auth): address onboarding review follow-ups 2026-04-29 19:10:30 -07:00
Claude
236e896189
fix(scim): block virtual keys when SCIM deprovisions/deactivates a user
Previously, deleting a user via SCIM (`DELETE /scim/v2/Users/{id}`) or
marking them inactive (`PATCH active=false` / `PUT active=false`) only
touched the user row. Their virtual keys kept working because:

- `litellm_verificationtoken` was never updated.
- The auth path's combined-view query on the key never joined to the
  user's active state.
- `get_user_object()` was wrapped in a silent `except` that set
  `user_obj=None` when the owning user record was gone, so requests
  proceeded normally.

Changes:

- Add `_set_user_keys_blocked(user_id, blocked)` in scim_v2.py that
  flips only mismatched rows via `update_many` and invalidates each
  affected token in the dual cache.
- Cascade SCIM lifecycle events to keys:
  - `delete_user`: block all of the user's keys before deleting the
    user row (preserves spend/audit while orphaning safely).
  - `patch_user` / `update_user`: on `scim_active` transitions,
    block (false) or unblock (true) the user's keys.
- Defense in depth in `user_api_key_auth`: reject the request when the
  loaded `user_obj` has `metadata.scim_active == False`, even if a
  cached key snuck past the per-key block.
- `transform_litellm_user_to_scim_user` now reflects the real
  `scim_active` value instead of always returning `active=True`.

Tests:
- New `test_scim_key_deactivation.py` covering DELETE, PATCH
  active=false, PATCH active=true, no-op patches, and the helper's
  cache-invalidation contract.
- New `test_scim_deactivated_user_key_is_rejected` exercising the
  auth-path defense.
- Existing PATCH tests updated with verificationtoken mocks for the
  new code path.
2026-04-30 02:01:23 +00:00
user
e0b32eb1cf fix(mcp): re-encrypt user credentials during master-key rotation
Greptile P1: this PR encrypts LiteLLM_MCPUserCredentials rows under the
salt key, but the /key/regenerate rotation endpoint had no
corresponding step for that table.  Rotating the master key would
leave every BYOK and OAuth2 user credential permanently unreadable.

Adds rotate_mcp_user_credentials_master_key, mirroring the existing
rotate_mcp_server_credentials_master_key pattern: read each row with
the current key (via _decode_user_credential, which also handles
unmigrated legacy plaintext rows), re-encrypt under the new master
key, write back.  One bad row is logged and skipped instead of
aborting the whole rotation.

Wired into key_management_endpoints.py as step 4b, alongside the
existing server-credentials rotation, with the same try/except shape
so a transient DB error on this table doesn't kill the whole
regenerate-key flow.

Tests cover: round-trip through rotation under a new key, automatic
re-encryption of legacy plaintext rows (rotation also acts as a
migration trigger), and a corrupt row not aborting the rotation.
2026-04-30 01:58:26 +00:00
user
8205e248a6 chore(auth): harden invite-link onboarding token flow 2026-04-29 18:55:11 -07:00
user
c3fcafb1bf fix(mcp): warn once when use_x_forwarded_for is on but no trusted ranges
Greptile P1: deployments that today have ``use_x_forwarded_for: true``
but never configured ``mcp_trusted_proxy_ranges`` would silently see
their MCP OAuth discovery URLs revert to the proxy's literal bind
address after this change, with no log line explaining why.

Emit a one-shot WARNING the first time the gate denies for that
specific reason, telling the operator exactly which setting to add.
The warning is module-scoped (not per-request) so the proxy log
stays quiet after the first hit.
2026-04-30 01:46:42 +00:00
user
8af0544ef0 chore(mcp): tighten OAuth root endpoint resolution 2026-04-29 18:36:17 -07:00
user
463781d8cb chore(mcp): gate X-Forwarded-* trust on get_request_base_url
get_request_base_url unconditionally honoured X-Forwarded-Proto / Host /
Port to build OAuth issuer / redirect_uri / authorization_endpoint
values for the MCP discovery endpoints.  In a deployment where the
proxy is reachable from a caller that can send those headers (direct
internet exposure, or a reverse proxy that does not strip them), an
attacker could poison the OAuth metadata and steer MCP clients at an
attacker-controlled host.

Apply the same trusted-proxy gate the codebase already uses for
get_mcp_client_ip: only honour the headers when use_x_forwarded_for is
enabled in proxy settings AND the direct connection IP falls inside
mcp_trusted_proxy_ranges.  When that's not configured, fall back to
the request's literal base_url, so an untrusted caller cannot poison
the discovery metadata.

The existing X-Forwarded-* parsing test cases now opt into a
trust_xff fixture (the parsing logic itself is unchanged).  Adds a
matrix for the new gate covering: XFF disabled, XFF enabled with no
ranges, caller outside ranges, caller inside ranges, and the
loopback-dev-deployment case.
2026-04-30 01:30:41 +00:00
Yuneng Jiang
9b019aaa6b Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_fix-config-update-targeted-upserts 2026-04-29 18:26:19 -07:00
user
c76c300392 fix(mcp): address Greptile P2s on credential encoding helpers
Three minor fixes from Greptile review:

1. _decode_user_credential now also catches TypeError so a null
   credential_b64 value returns None instead of propagating, matching
   the documented "returns None when neither path yields a valid
   string" contract.

2. The OAuth2 BYOK guard error no longer claims the existing row is a
   BYOK credential — after a salt-key rotation, an OAuth2 row can fail
   to decrypt and reach the same guard.  Reword to "could not be
   verified as an OAuth2 token", which is accurate for both cases.

3. Drop the no-op sys.path.insert in the new test file (other tests
   in the directory don't need it; pytest picks up the package via
   the installed editable wheel).

Adds a regression test for the None-input case.
2026-04-30 00:44:37 +00:00
user
6293252f93 harden cli sso review findings 2026-04-29 17:34:09 -07:00
Yuneng Jiang
db5cdfc440 fix(proxy): /config/update litellm_settings merge — request wins
Flip the litellm_settings dict merge from {**incoming, **existing} to
{**existing, **incoming} so the caller's value for any pre-existing key
is what gets persisted. The previous direction silently no-op'd a
request like {"litellm_settings": {"drop_params": false}} when the DB
already held drop_params: true — the endpoint returned 200 OK but the
stored value never changed. router_settings (immediately below) had
been doing the right thing all along; this brings the two sections into
alignment.

success_callback semantics are unchanged: it is still always normalized
to lowercase, and still unioned with any existing list (callbacks are
additive — a caller sends the new entry, not the full set).

Adds a regression test (drop_params: True in DB, request flips to
False, expect persisted False with other keys preserved).
2026-04-29 17:28:04 -07:00
stuxf
dedaf74a5e
chore(auth): tighten clientside api_base handling (#26518)
* chore(auth): validate clientside api_base against SSRF guard; clear admin secrets on base override

Two related issues with how the proxy handles client-supplied
``api_base`` / ``base_url`` overrides on chat-completion requests:

1. **SSRF gate bypass** — ``check_complete_credentials()`` returned
   ``True`` for any non-empty ``api_key``, allowing the
   ``is_request_body_safe`` ``banned_params`` loop to admit ``api_base``
   / ``base_url`` values that point at private (RFC 1918), loopback,
   link-local, or cloud-metadata addresses. Now: when the gate sees a
   client-supplied ``api_base`` / ``base_url``, it runs the URL through
   ``litellm_core_utils.url_utils.validate_url`` (DNS-resolves, blocks
   internal/IMDS/LL networks, defends against rebinding). Rejection
   raises with a clear message.

2. **Admin-config leak on base override** —
   ``get_dynamic_litellm_params`` only carried the three clientside keys
   (``api_key``, ``api_base``, ``base_url``) from request to upstream
   call. Other admin-configured fields on ``litellm_params`` —
   ``organization``, ``extra_body``, ``extra_headers``, ``api_version``,
   ``azure_ad_token``, AWS / Vertex creds, etc. — flowed through
   unchanged. With base redirected to a client-controlled server, those
   admin secrets were sent to the attacker. Now: when ``api_base`` /
   ``base_url`` is in ``request_kwargs``, drop those admin-config
   fields from ``litellm_params`` unless the caller re-supplied them.

Tests cover the SSRF-target rejection per URL field, the admin-secret
clearing on base override, the don't-clear case when only ``api_key``
is overridden (BYOK pattern), and the don't-overwrite case when the
caller resupplies fields like ``organization`` themselves.

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

* chore(vertex-batches): wrap api_base GET in safe_get for defense-in-depth

The vertex batches status-poll fetches an attacker-influenceable
``api_base`` URL with a raw ``sync_handler.get()``. The proxy auth gate
already validates clientside ``api_base`` before reaching this sink, so
the proxy flow is covered. This adds the per-sink wrap so SDK callers
and any future code path that bypasses the proxy gate pick up the same
SSRF defense from ``url_utils.safe_get``.

Operators with a legitimate private Vertex base can either allowlist
the host via ``litellm.user_url_allowed_hosts`` or disable validation
with ``litellm.user_url_validation = False``.

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

* refactor(auth): hoist url_utils import; derive admin-config field list from CredentialLiteLLMParams

/simplify pass:
- Move ``from litellm.litellm_core_utils.url_utils import SSRFError, validate_url``
  to module top in ``proxy/auth/auth_utils.py``. CLAUDE.md prefers
  module-level imports unless avoiding a circular dependency, and
  there's no cycle here (``url_utils`` doesn't depend on ``proxy.auth``).
- Replace the hardcoded ``_ADMIN_CONFIG_FIELDS_TO_CLEAR_ON_BASE_OVERRIDE``
  literal with ``_admin_config_fields_to_clear_on_base_override()`` that
  derives the typed-field portion from
  ``CredentialLiteLLMParams.model_fields``. Adds three fields the
  hardcoded list missed (``aws_bedrock_runtime_endpoint``,
  ``watsonx_region_name``, ``region_name``) and stays in sync as new
  provider fields are declared on the model. The kwargs-only set
  (``organization``, ``extra_body``, ``azure_ad_token``, ``aws_session_token``,
  ``aws_sts_endpoint``, ``aws_web_identity_token``, ``aws_role_name``, …)
  remains explicit since those fields aren't on the typed model.

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

* fix(auth): close field-echo bypass; gate URL check on toggle; cover async batch path

Three issues from review:

1. ``get_dynamic_litellm_params`` used ``if field not in request_kwargs:
   pop`` to clear admin-set provider config when the caller redirected
   ``api_base``. A caller could *echo* any clear-list field name (with any
   value, including an empty string) to skip the pop, leaving the admin's
   value in ``litellm_params`` to be forwarded to the redirected upstream.
   Fix: always pop, then write the caller's value back if they resupplied
   the field.

2. ``check_complete_credentials`` called ``validate_url`` directly. That
   helper doesn't itself consult ``litellm.user_url_validation``; the
   toggle is honoured by ``safe_get`` / ``async_safe_get``. Mirror that
   here so admins who explicitly disabled URL validation aren't blocked
   at the proxy boundary.

3. ``VertexAIBatchesHandler._async_retrieve_batch`` still used a bare
   ``await client.get(api_base, ...)`` while the sync sibling was wrapped
   in ``safe_get``. Wrap the async call in ``async_safe_get`` so SDK
   callers on the async path get the same DNS-rebind / private /
   cloud-metadata defenses as the sync path.

Tests:

- ``TestCheckCompleteCredentialsBlocksSSRF`` is now mock-only; an autouse
  fixture flips the toggle on, ``validate_url`` is patched in the
  parametrized blocking tests, and the positive path no longer makes a
  real DNS call to api.openai.com.
- ``test_skips_url_validation_when_toggle_is_off`` documents the new
  toggle-off behaviour and asserts ``validate_url`` is not called.
- ``test_caller_resupplied_value_overrides_admin_value_on_base_override``
  replaces the prior test that asserted the buggy
  preserve-admin-value-on-echo behaviour.
- ``test_field_echo_does_not_preserve_admin_value`` is a focused
  regression test for the empty-string echo vector.

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

* fix(auth): close provider-confusion credential exfil; expand banned-params; cover OCI

Three additions on top of the entry-point URL gate so the cluster is
fully closed against caller-supplied ``api_base`` redirection:

1. ``get_llm_provider_logic.py`` matched registered openai-compatible
   endpoints against ``api_base`` with an unanchored substring search
   (``if endpoint in api_base:``). A caller could pass an api_base like
   ``https://attacker.com/api.groq.com/openai/v1`` to coerce the proxy
   into reading ``GROQ_API_KEY`` from the environment and forwarding it
   as a Bearer credential to the attacker's host. Replaced with parsed-
   URL semantics (hostname exact-match plus segment-bounded path-prefix)
   in a new ``_endpoint_matches_api_base`` helper.

2. ``is_request_body_safe`` rejects ``api_base`` / ``base_url`` /
   ``user_config`` / a handful of AWS / vertex fields, but the list
   omitted three other endpoint-targeting fields:
   * ``aws_bedrock_runtime_endpoint`` — Bedrock endpoint redirect
   * ``langsmith_base_url`` / ``langfuse_host`` — observability callback
     hostnames; attacker-controlled values exfiltrate the entire request
     payload (incl. message content) via the logging hook.
   Added all three to the blocklist.

3. ``_admin_config_fields_to_clear_on_base_override`` derives its typed-
   field list from ``CredentialLiteLLMParams.model_fields``, which does
   not declare any of the OCI provider's auth fields. Added
   ``oci_signer``, ``oci_user``, ``oci_fingerprint``, ``oci_tenancy``,
   ``oci_key``, and ``oci_key_file`` to the kwargs-only fixed list so
   they are cleared on caller-redirected ``api_base`` like the AWS /
   Azure / Vertex equivalents.

Tests:

- ``TestEndpointMatchesApiBase`` — direct unit tests on the new
  matcher: legitimate provider URLs (5 shapes) match; attacker
  smuggling via path injection, suffix label, prefix label, userinfo
  ``@`` injection, and path-segment lookalikes (7 shapes) do not.
- ``TestGetLlmProviderRejectsAttackerSmuggledApiBase`` — end-to-end
  invariant that ``GROQ_API_KEY`` is never read against an attacker-
  controlled host while the legitimate ``api.groq.com`` path still
  resolves the provider correctly.
- ``TestIsRequestBodySafeBlocksEndpointTargetingFields`` — parametrized
  coverage that each of the three new banned-params raises a clear
  rejection naming the offending field.

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

* fix(auth): remove implicit api-key bypass + add posthog/braintrust/slack to blocklist

The historical ``check_complete_credentials`` clause inside
``is_request_body_safe`` was a third, *implicit*, *caller-controlled*
BYOK path: any caller that supplied a non-empty ``api_key`` caused the
entire banned-params blocklist to be skipped. That turned every missing
entry on the blocklist into an exploitable SSRF / credential-exfil hole
and is the root cause of the chain of api_base advisories that have
been re-discovered with each new integration:

* GHSA-jh89-88fc-qrfp (critical, triage) — env-var exfil via api_base
* GHSA-3frq-6r6h-7j64 (high, triage) — admin org / extra_body leak
* veria-admin Dv_m860l, b_yRJeQ5, stN90yjP, LBlyOAc8, U2TD78kg —
  variations on "list X is missing field Y"

Two explicit, admin-controlled BYOK paths already exist and remain:
``general_settings.allow_client_side_credentials = true`` (proxy-wide)
and ``configurable_clientside_auth_params: [...]`` per deployment.
Removing the implicit bypass converts the failure mode of a missing
blocklist entry from "live credential leak" to "predictable 400 with
a clear remediation message," which is the structural fix.

Also adds the three remaining endpoint-targeting fields the dynamic
callback layer reads from request body: ``posthog_host``,
``braintrust_host``, ``slack_webhook_url``. ``slack_webhook_url`` in
particular was a direct exfil channel (caller-set webhook → proxy
mirrors every request to attacker's Slack).

Tests:

- ``test_api_key_does_not_bypass_blocklist`` — parametrized regression
  asserting api_key=anything no longer skips the gate for any of the
  five highest-risk fields.
- ``test_admin_opt_in_proxy_wide_still_allows`` — confirms the
  documented BYOK opt-in still works.
- Extends ``test_endpoint_targeting_field_in_request_body_is_rejected``
  to cover posthog / braintrust / slack.

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

* fix(auth): block sagemaker_base_url, s3_endpoint_url, deployment_url

Provider-specific endpoint overrides surfaced by a wider audit of
``optional_params`` consumers in ``litellm/llms/``. Same threat as
``api_base``: a caller-supplied value redirects the outbound request
to an attacker host.

* ``s3_endpoint_url`` — read in ``litellm/llms/bedrock/files/transformation.py``
  to build the S3 upload URL for Bedrock files. Caller redirects file
  uploads to attacker-controlled S3.
* ``sagemaker_base_url`` — read in ``litellm/llms/sagemaker/{chat,completion}/*``.
  Caller redirects SageMaker traffic. This is the primary vector
  described in veria-admin mNqEBBtG.
* ``deployment_url`` — popped in ``litellm/llms/sap/chat/transformation.py``.
  Caller redirects SAP deployment requests.

Tests parametrize ``test_endpoint_targeting_field_in_request_body_is_rejected``
to cover the three new fields.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 17:27:22 -07:00
user
f3000bda36 chore(mcp): encrypt user-scoped credentials at rest
LiteLLM_MCPUserCredentials.credential_b64 stored both BYOK API keys and
OAuth2 access tokens as plain urlsafe-base64 of the raw value. Any DB
read could recover the upstream-provider key.

Run all writes through encrypt_value_helper (nacl SecretBox, the same
helper used for the server-level credentials column) and read back via
a small dual-path helper that tries decryption first, then falls back to
plain base64 so existing rows keep working until they get rewritten.

Folds the three near-identical "decode -> json.loads -> check type ==
oauth2" sites into _decode_oauth_payload, which simplifies the BYOK
guard inside store_user_oauth_credential.
2026-04-30 00:27:15 +00:00
user
19ca420056 cover cli sso start validation 2026-04-29 17:25:32 -07:00
user
2612187c57 fix cli auth test expectations 2026-04-29 17:24:17 -07:00
Michael Riad Zaky
0f8dd28542 lazy-load optional feature routers on first request 2026-04-29 17:20:55 -07:00
user
88d8a80761 tighten cli sso session flow 2026-04-29 17:13:25 -07:00
ishaan-berri
4a7af1ff68
feat(proxy): durable agent workflow run tracking via /v1/workflows/runs (#26793)
* feat(schema): add workflow run tracking tables (LiteLLM_WorkflowRun, LiteLLM_WorkflowEvent, LiteLLM_WorkflowMessage)

* feat(proxy): add /v1/workflows/runs endpoints for durable agent workflow tracking

* feat(proxy): register workflow management router in proxy_server

* docs(workflows): add README for workflow run tracking API

* test(workflows): add unit tests for /v1/workflows/runs endpoints

* fix(workflows): atomic event+status update via tx(), run_id 404 guard, sequence retry on collision

* test(workflows): add tx mock, 404 on unknown run_id, retry-on-collision tests

* fix(workflows): constrain status to Literal enum, rename total→count in list responses

* add tenant isolation and bounded limits to workflow endpoints

* add created_by column and index to LiteLLM_WorkflowRun

* add ownership and bounded-limit tests for workflow endpoints

* Fix workflow run ownership for null owners

* guard prisma import in workflow_management_endpoints

* sync schema.prisma copies with workflow run models

* black: format workflow_management_endpoints.py

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-04-29 17:12:18 -07:00
user
722bc63e37
chore(oauth2-proxy): drop unused patch import + tighten docstring
Greptile flagged the unused ``from unittest.mock import patch``
left over from before the ``configure_proxy`` fixture refactor (the
fixture uses ``monkeypatch``, no ``patch`` calls remain). Also pruned
the now-stale "premium gate" paragraph from the module docstring
since that gate was removed in fbcfd59b1a.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 23:47:29 +00:00
Ryan Crabbe
2461139593
fix(proxy): inherit caller identity in passthrough batch managed-object
Read user_id and team_id from the request's litellm_params metadata when
fabricating the UserAPIKeyAuth handed to the managed_files hook, so
batches created via passthrough are attributed to the real requester
instead of a hardcoded fallback. Adds parametrized regression coverage
for both the populated-metadata and empty-kwargs cases.
2026-04-29 16:25:13 -07:00
Yuneng Jiang
1fd38eb5a5 fix(proxy): /config/update normalize existing success_callback before dedup
When a litellm_settings row already holds mixed-case names (e.g.
["Langfuse"]) — written by another code path or by hand — the
union-on-update path was running set([...]) over the raw existing list
plus the lowercase-normalized incoming list, so "Langfuse" and
"langfuse" survived as duplicates. delete_callback uses a lowercase
lookup, leaving the mixed-case entry unreachable.

Normalize the existing list with normalize_callback_names before the
union so the merged list converges to lowercase. Adds a regression test
covering the case where the DB starts with ["Langfuse", "SQS"] and the
caller submits ["langfuse"].
2026-04-29 16:21:51 -07:00
user
148485c2a2
fix(passthrough): default auth=True; drop enterprise gate on the safe option
Pass-through endpoints configured in
``general_settings.pass_through_endpoints`` defaulted to ``auth: false``
and the safe ``auth: true`` setting was rejected at startup unless the
operator had a LiteLLM Enterprise license. Net result: OSS deployments
had **no safe configuration** — every pass-through admins added without
remembering ``auth: true`` shipped an unauthenticated forwarder, and
remembering ``auth: true`` raised a hard "enterprise-only" error.

Three changes:

* ``litellm/proxy/_types.py`` — flip
  ``PassThroughGenericEndpoint.auth`` default to ``True``. Operators
  who add a pass-through with no explicit ``auth`` value now get a
  safe, authenticated forwarder by default. Setting ``auth: false``
  remains supported for genuine public-forwarder use cases (e.g.
  webhook receivers).

* ``litellm/proxy/pass_through_endpoints/pass_through_endpoints.py``
  — drop the ``premium_user`` gate around ``auth: true``. An
  unauthenticated forwarder is a deployment choice operators should
  be allowed to make explicitly, but the safe option must always be
  free. The product-tier decision (which features sit behind the
  enterprise license) is separate from "OSS users must always have a
  safe option."

* ``litellm/proxy/auth/user_api_key_auth.py`` — the runtime dispatch
  pulls pass-through endpoints from ``general_settings`` as raw
  dicts, so the Pydantic default doesn't apply. Switched
  ``endpoint.get("auth")`` to ``endpoint.get("auth", True)`` so a
  config dict without an explicit ``auth`` key still requires
  authentication at request time.

Tests:

- ``test_passthrough_auth_defaults_to_true`` — Pydantic default is
  now safe.
- ``test_passthrough_auth_can_still_be_explicitly_disabled``
  — opt-in to ``auth=False`` still works for legitimate
  public-forwarder use cases.
- ``test_register_passthrough_with_auth_true_works_for_oss``
  — ``premium_user=False`` no longer rejects ``auth=true``.
- ``test_runtime_check_treats_missing_auth_key_as_authenticated``
  — raw dict without an ``auth`` key now requires auth (the
  previously-unauthenticated forwarder).
- ``test_runtime_check_explicit_auth_false_still_skips_validation``
  — explicit opt-in still works.

Closes GHSA-7h34-mmrh-6g58.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 23:10:59 +00:00
yuneng-jiang
fc0cc9c581
Merge pull request #26225 from BerriAI/litellm_dbReconnectNonBlocking
[Fix] Proxy: reconnect Prisma DB without blocking the event loop
2026-04-29 16:09:22 -07:00
user
fbcfd59b1a
fix(oauth2-proxy): drop premium gate; identity-only allowlist is the security fix
Greptile flagged the ``premium_user is not True`` check as a hard
backwards-incompatible break for OSS users currently running
``enable_oauth2_proxy_auth=True``. They were right: unlike the
api_base case (where the docs already required admin opt-in), this
path was documented as available to OSS users. Adding the gate would
have closed a documented feature, not fixed a vuln.

Reframed the change:

* The **identity-only allowlist** (``ALLOWED_OAUTH2_PROXY_FIELDS`` =
  ``{user_id, user_email, team_id, team_alias, org_id, models}``) is
  the actual security fix — it closes the privesc by rejecting any
  mapping to a non-identity field at request time. This is unchanged.
* The **premium gate** was parity-with-siblings (a product decision,
  not a security one). Removed. BerriAI can re-add it on their own
  schedule with a proper deprecation cycle if they want enterprise-
  only gating.

Tests: removed ``test_rejects_when_not_premium``; everything else
(allowlist enforcement, identity passthrough, attack-shape
regression) still passes — 14 tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 23:04:05 +00:00