fix(proxy): close 2 leak vectors flagged by veria-ai on #29748

veria-ai's review of a501b0e34e raised two Medium-severity findings:

1. /v2/model/info default branch did not apply the BYOK team-scope
   filter when caller's key/team/user model lists were all empty.
   Result: a virtual key with empty restrictions could see other
   teams' BYOK deployment metadata (api_base, model_info.id, etc.).

2. /v1/model/info?litellm_model_id=<id> (by-id branch) applied only
   the user.models filter after the option-B refactor, skipping
   key.models / team_models / BYOK scope. A key restricted by
   key.models with no personal model restriction could pull metadata
   for any deployment.

Finding #2 is a regression introduced by switching the by-id branch
from a 403 gate to filter-chain semantics; the option-B refactor
applied only the personal-models filter and missed the listing-path's
key/team intersection plus the BYOK scope filter that the v1 default
branch already used.

Fix mirrors the v1 default branch's filter chain on the by-id path
and adds the BYOK filter to v2:
- v1 by-id: insert _get_v1_model_info_allowed_model_names +
  _filter_v1_model_info_deployments (key/team), keep the existing
  user.models filter, append the BYOK _get_caller_byok_team_scope +
  _byok_row_outside_caller_teams filter.
- v2 default: append the same BYOK filter after user.models.

All filters use existing helpers; no new code paths introduced.
Filter (drop deployment → empty data) preserved instead of 403, per
the route_checks.py:189 spec.

Local verification: 87/87 across
- tests/test_litellm/proxy/discovery_endpoints/
- tests/test_litellm/proxy/proxy_server/test_routes_model_info.py
- tests/test_litellm/proxy/proxy_server/test_team_model_name_translation.py
This commit is contained in:
songkuan-zheng 2026-06-17 11:56:15 +00:00
parent 05a9d313ac
commit da700843cb

View file

@ -12707,6 +12707,24 @@ async def model_info_v2(
user_models_override=user_models_for_filter,
)
# Team BYOK deployments carry an internal routing key and other teams'
# public name / team_id / api_base; drop the ones the caller cannot
# access so a bare /v2/model/info (no user_models_only, no
# include_team_models) does not leak cross-team metadata when the
# caller's key/team/user lists are empty. Mirrors the v1 default
# branch behavior below.
allowed_team_ids = await _get_caller_byok_team_scope(
user_api_key_dict=user_api_key_dict,
prisma_client=prisma_client,
)
all_models = [
model
for model in all_models
if not _byok_row_outside_caller_teams(
model.get("model_info") or {}, allowed_team_ids
)
]
# Apply teamId filter if provided
if teamId is not None and teamId.strip():
all_models = await _filter_models_by_team_id(
@ -13474,16 +13492,24 @@ async def model_info_v1(
llm_router=llm_router,
user_api_key_dict=user_api_key_dict,
)
# Bound by LiteLLM_UserTable.models (Personal Models) using the
# same deployment-level filter as the listing branch. by-id used
# to short-circuit before this filter, letting a restricted user
# pull any deployment's litellm_params via /v1/model/info?litellm_model_id=
# — same leak BerriAI/litellm#26420 fixed for /v1/models.
#
# Filter (drop unauthorized deployments → empty list) instead of
# 403: matches the route_checks.py:189 spec comment "/model/info
# just shows models user has access to" and avoids leaking model
# Bound by the same filter chain as the listing branch below
# (key.models / team_models, then LiteLLM_UserTable.models, then
# BYOK team scope). by-id used to short-circuit before any of
# these, letting a restricted virtual key pull any deployment's
# litellm_params via /v1/model/info?litellm_model_id=. Filter
# (drop unauthorized deployments → empty list) instead of 403:
# matches the route_checks.py:189 spec comment "/model/info just
# shows models user has access to" and avoids leaking model
# existence via 403-vs-404 enumeration.
allowed_model_names = _get_v1_model_info_allowed_model_names(
user_api_key_dict=user_api_key_dict,
llm_router=llm_router,
)
single_model_list = _filter_v1_model_info_deployments(
all_models=single_model_list,
allowed_model_names=allowed_model_names,
)
from litellm.proxy.utils import apply_user_models_filter_to_deployments
single_model_list = await apply_user_models_filter_to_deployments(
@ -13494,6 +13520,18 @@ async def model_info_v1(
proxy_logging_obj=proxy_logging_obj,
user_api_key_cache=user_api_key_cache,
)
allowed_team_ids = await _get_caller_byok_team_scope(
user_api_key_dict=user_api_key_dict,
prisma_client=prisma_client,
)
single_model_list = [
model
for model in single_model_list
if not _byok_row_outside_caller_teams(
model.get("model_info") or {}, allowed_team_ids
)
]
return {"data": single_model_list}
# Return router deployments (same source as /v2/model/info), not wildcard-