Skip to content

chore(proxy): close callback-config and observability-credential side channels - #27081

Merged
yuneng-berri merged 2 commits into
BerriAI:litellm_internal_stagingfrom
stuxf:fix/strip-callback-fields
May 4, 2026
Merged

chore(proxy): close callback-config and observability-credential side channels#27081
yuneng-berri merged 2 commits into
BerriAI:litellm_internal_stagingfrom
stuxf:fix/strip-callback-fields

Conversation

@stuxf

@stuxf stuxf commented May 3, 2026

Copy link
Copy Markdown
Collaborator

Type

🐛 Bug Fix
✅ Test

Changes

Two related gaps in the proxy's request bouncer:

  1. is_request_body_safe (auth_utils.py) previously walked the request-body root and the litellm_embedding_config nested dict, but not metadata / litellm_metadata. The same fields it bans at the 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 credentials. Walk both metadata containers (and parse the JSON-string form sent via multipart / extra_body) through the same banned-params helper, so the existing general_settings.allow_client_side_credentials opt-in covers both paths consistently.

  2. Derived ban list. The banned-params list was hand-maintained and lagged the canonical _supported_callback_params allow-list in litellm/litellm_core_utils/initialize_dynamic_callback_params.py. 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 new integrations are covered automatically; _EXTRA_BANNED_OBSERVABILITY_PARAMS carries the handful of fields integrations read but the allow-list hasn't caught up to (posthog_api_url, phoenix_project_name, wandb_api_key, weave_project_id). A guard test fails CI if a new entry is added to _supported_callback_params without an explicit safe-list decision.

  3. Control-field strip 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.

Compatibility

  • SDK callers (import litellm; litellm.completion(...)): unaffected. Both changes only run in the proxy ingress path.
  • Operators using request-body or metadata observability creds (BYO Langfuse / Langsmith / etc. on a multi-tenant proxy): flip general_settings.allow_client_side_credentials = true if they haven't already — the same flag that already gates the root path. Operators using only the metadata path were inadvertently bypassing this gate; this PR closes that gap.
  • Per-request informational observability metadata (langfuse_session_id, langfuse_tags, trace_id, helicone_meta, langsmith_metadata, span identifiers, lago_api_event_code, openmeter_event_type, etc.): unaffected — these don't choose the destination or the credentials.
  • callbacks / service_callback / logger_fn / litellm_disabled_callbacks in request bodies: stripped silently; no documented per-request use, the legitimate equivalent is admin-configured callbacks on the proxy.

Test Plan

  • tests/test_litellm/proxy/auth/test_auth_utils.py — adds TestObservabilityCallbackBans: each new field rejected at root, each rejected from metadata and litellm_metadata (dict + JSON-string forms), allow_client_side_credentials allows the metadata BYO path, informational langfuse_prompt_version / langsmith_sampling_rate still pass through. Guard test asserts coverage of _supported_callback_params.
  • tests/test_litellm/proxy/test_litellm_pre_call_utils.py — parametrized test that each of the four new control fields is stripped from both data and the post-strip proxy_server_request.body snapshot.
uv run pytest tests/test_litellm/proxy/auth/test_auth_utils.py tests/test_litellm/proxy/test_litellm_pre_call_utils.py -q
261 passed in 63.45s

uv run black --check litellm/proxy/auth/auth_utils.py litellm/proxy/litellm_pre_call_utils.py tests/test_litellm/proxy/auth/test_auth_utils.py tests/test_litellm/proxy/test_litellm_pre_call_utils.py
All done!

🤖 Generated with Claude Code

… 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>
@stuxf

stuxf commented May 3, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai

@codecov

codecov Bot commented May 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented May 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR closes two observability side-channel gaps in the proxy's request bouncer: it extends the banned-param check to walk metadata / litellm_metadata containers (including JSON-string-encoded blobs sent via multipart), derives the ban list automatically from _supported_callback_params so new integrations are covered without manual updates, strips four callback-registration fields from root request data to prevent worker-wide callback poisoning, and fixes the previously flagged returncontinue bug in _check_banned_params so a model-level-allowed param can no longer shadow subsequent banned params in the same request body.

Confidence Score: 5/5

Safe to merge — all three gaps are correctly closed, the previously flagged P1 (return→continue) is fixed, and tests cover root/metadata/JSON-string paths plus a guard test for the canonical allow-list.

No P0 or P1 issues found. The return→continue regression is corrected, the metadata walk and derived ban list are correctly implemented, circular import is not a risk, and the callback control fields are stripped before any re-addition from trusted DB metadata. Only a minor style observation about hoisting the allow_client_side_credentials check before the loop.

No files require special attention.

Important Files Changed

Filename Overview
litellm/proxy/auth/auth_utils.py Core security fix: adds metadata path checking, derives observability ban list from canonical allow-list, and fixes the return→continue bug in _check_banned_params so a model-level-allowed param can't shadow subsequent banned params.
litellm/proxy/litellm_pre_call_utils.py Adds callbacks, service_callback, logger_fn, and litellm_disabled_callbacks to _UNTRUSTED_ROOT_CONTROL_FIELDS to prevent worker-wide callback poisoning and unauthorized observability disabling.
tests/test_litellm/proxy/auth/test_auth_utils.py Adds TestObservabilityCallbackBans covering root/metadata/JSON-string paths, allow_client_side_credentials opt-in, safe params passthrough, regression test for the return→continue fix, and guard test ensuring _supported_callback_params is fully covered.
tests/test_litellm/proxy/test_litellm_pre_call_utils.py Parametrized test verifying each of the four new control fields is stripped from both data and the proxy_server_request.body snapshot; no real network calls made.

Reviews (2): Last reviewed commit: "fix(auth): per-param allow must continue..." | Re-trigger Greptile

Comment on lines +194 to +198
_SAFE_CLIENT_CALLBACK_PARAMS: FrozenSet[str] = frozenset(
{
"langfuse_prompt_version",
"langsmith_sampling_rate",
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 _SAFE_CLIENT_CALLBACK_PARAMS may be too narrow for session/trace fields

The safe-list currently covers only langfuse_prompt_version and langsmith_sampling_rate. The PR description explicitly calls out langfuse_session_id, langfuse_tags, trace_id, helicone_meta, langsmith_metadata, span identifiers, lago_api_event_code, and openmeter_event_type as unaffected — these don't choose the destination or credentials. These fields are not in _supported_callback_params today (so they won't be banned), but if any get added to that canonical list later they will be banned by default and break existing callers unless this safe-list is updated simultaneously.

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>
@stuxf

stuxf commented May 3, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai

@yuneng-berri
yuneng-berri merged commit e4ac46b into BerriAI:litellm_internal_staging May 4, 2026
43 checks passed
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
chore(proxy): close callback-config and observability-credential side channels
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
…dy bouncer

Two cleanups:

* ``LiteLLMSkillsHandler.create_skill`` raised ``HTTPException`` for
  identity-less callers, importing FastAPI from a ``litellm/llms/``
  module — that violates the project rule that FastAPI lives only
  under ``proxy/``. Switch to ``ValueError`` (the same shape the rest
  of the handler uses for not-found/forbidden) and update the test.

* The proxy-auth body bouncer derived its observability ban list from
  ``_supported_callback_params`` only, missing
  ``_request_blocked_callback_params`` (where ``gcs_bucket_name`` and
  ``gcs_path_service_account`` live). Two recently-merged sibling PRs
  (BerriAI#27019 added the deny list, BerriAI#27081 added the test asserting these
  are rejected at the request body root) crossed without folding them
  together. Union the GCS deny list into the bouncer's derivation so
  the single source of truth covers both code paths.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants