mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix(proxy): strip callback credentials from the auth object stamped into request metadata (#37233)
Some checks failed
CI Coverage / assert-ci-coverage (push) Waiting to run
CodSpeed Benchmarks / benchmarks (push) Waiting to run
Publish basedpyright base counts / publish (push) Waiting to run
Code Quality Checks / code-quality (push) Waiting to run
UI Unit Tests / ui-unit-tests (push) Waiting to run
Unit Tests: Core Utilities / core-utils (push) Waiting to run
Unit Tests: Documentation Validation / documentation (push) Waiting to run
Unit Tests: Enterprise, Google GenAI & Routing / enterprise-routing (push) Waiting to run
Unit Tests: Integrations (Callbacks & Logging) / integrations (push) Waiting to run
Unit Tests: LLM Provider Transformations / All Other Providers (push) Waiting to run
Unit Tests: LLM Provider Transformations / Vertex AI (push) Waiting to run
Unit Tests: MCP, Secrets, Containers & Misc / misc (push) Waiting to run
Unit Tests: Proxy Auth & Key Management / proxy-auth (push) Waiting to run
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Waiting to run
Unit Tests: Proxy DB Operations / auth-checks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / budgets (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / custom-logging (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / db-and-spend (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / endpoints-and-responses (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / guardrails-hooks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / jwt-and-keys (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / key-generation (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / logging-misc (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-runtime (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-server-core (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-utils (push) Blocked by required conditions
Unit Tests: Proxy API Endpoints / proxy-endpoints (push) Waiting to run
Unit Tests: Proxy API Endpoints / proxy-server (push) Waiting to run
Unit Tests: Proxy Infrastructure / proxy-infra (push) Waiting to run
Unit Tests: Responses, Caching & Types / responses-caching-types (push) Waiting to run
GitHub Actions Security Analysis / zizmor (push) Waiting to run
Terraform Modules / fmt, validate, test (aws) (push) Has been cancelled
Some checks failed
CI Coverage / assert-ci-coverage (push) Waiting to run
CodSpeed Benchmarks / benchmarks (push) Waiting to run
Publish basedpyright base counts / publish (push) Waiting to run
Code Quality Checks / code-quality (push) Waiting to run
UI Unit Tests / ui-unit-tests (push) Waiting to run
Unit Tests: Core Utilities / core-utils (push) Waiting to run
Unit Tests: Documentation Validation / documentation (push) Waiting to run
Unit Tests: Enterprise, Google GenAI & Routing / enterprise-routing (push) Waiting to run
Unit Tests: Integrations (Callbacks & Logging) / integrations (push) Waiting to run
Unit Tests: LLM Provider Transformations / All Other Providers (push) Waiting to run
Unit Tests: LLM Provider Transformations / Vertex AI (push) Waiting to run
Unit Tests: MCP, Secrets, Containers & Misc / misc (push) Waiting to run
Unit Tests: Proxy Auth & Key Management / proxy-auth (push) Waiting to run
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Waiting to run
Unit Tests: Proxy DB Operations / auth-checks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / budgets (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / custom-logging (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / db-and-spend (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / endpoints-and-responses (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / guardrails-hooks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / jwt-and-keys (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / key-generation (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / logging-misc (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-runtime (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-server-core (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-utils (push) Blocked by required conditions
Unit Tests: Proxy API Endpoints / proxy-endpoints (push) Waiting to run
Unit Tests: Proxy API Endpoints / proxy-server (push) Waiting to run
Unit Tests: Proxy Infrastructure / proxy-infra (push) Waiting to run
Unit Tests: Responses, Caching & Types / responses-caching-types (push) Waiting to run
GitHub Actions Security Analysis / zizmor (push) Waiting to run
Terraform Modules / fmt, validate, test (aws) (push) Has been cancelled
* fix(proxy): strip callback credentials from the auth object stamped into request metadata * style(proxy): drop the restating half of the stamp-site comment * test(proxy): pin that the stamped auth copy carries header-derived identity
This commit is contained in:
parent
4d57bf0bdd
commit
a738c45fc7
4 changed files with 180 additions and 10 deletions
|
|
@ -39,10 +39,10 @@ _EXTRA_SENSITIVE_CALLBACK_KEYS: Final = {"gcs_path_service_account"}
|
|||
# already-encrypted input cheaply (no decrypt-attempt round trip) and
|
||||
# avoid double-encrypting if `LITELLM_SALT_KEY` is rotated between writes.
|
||||
_CALLBACK_VAR_ENCRYPTED_PREFIX: Final = "litellm_enc::"
|
||||
# Metadata slots that hold operator-configured callback setup (and therefore
|
||||
# integration credentials). Resolved from UserAPIKeyAuth during pre-call setup,
|
||||
# never read back off the copies stamped into request metadata.
|
||||
_CALLBACK_CONFIG_SLOTS: Final = frozenset({"logging", "callback_settings"})
|
||||
# Metadata slots that hold operator-configured callback and secret-manager setup
|
||||
# (and therefore integration credentials). Resolved from UserAPIKeyAuth during
|
||||
# pre-call setup, never read back off the copies stamped into request metadata.
|
||||
_CALLBACK_CONFIG_SLOTS: Final = frozenset({"logging", "callback_settings", "secret_manager_settings"})
|
||||
|
||||
blue_color_code: Final = "\033[94m"
|
||||
reset_color_code: Final = "\033[0m"
|
||||
|
|
|
|||
|
|
@ -1303,8 +1303,15 @@ class LiteLLMProxyRequestSetup:
|
|||
)
|
||||
if user_api_key_dict.budget_reservation is not None:
|
||||
data[_metadata_variable_name]["user_api_key_budget_reservation"] = user_api_key_dict.budget_reservation
|
||||
# Add the full UserAPIKeyAuth object for MCP server access control
|
||||
data[_metadata_variable_name]["user_api_key_auth"] = user_api_key_dict
|
||||
# UserAPIKeyAuth object for MCP server access control
|
||||
data[_metadata_variable_name]["user_api_key_auth"] = user_api_key_dict.model_copy(
|
||||
update={
|
||||
"metadata": strip_callback_config(user_api_key_dict.metadata),
|
||||
"team_metadata": strip_callback_config(user_api_key_dict.team_metadata),
|
||||
"project_metadata": strip_callback_config(user_api_key_dict.project_metadata),
|
||||
"organization_metadata": strip_callback_config(user_api_key_dict.organization_metadata),
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -1326,10 +1333,11 @@ class LiteLLMProxyRequestSetup:
|
|||
)
|
||||
|
||||
# ignore any special fields
|
||||
added_metadata: Final = {}
|
||||
for k, v in management_endpoint_metadata.items():
|
||||
if k not in (LiteLLM_ManagementEndpoint_MetadataFields_Premium + LiteLLM_ManagementEndpoint_MetadataFields):
|
||||
added_metadata[k] = v
|
||||
added_metadata: Final = {
|
||||
k: v
|
||||
for k, v in (strip_callback_config(management_endpoint_metadata) or {}).items()
|
||||
if k not in (LiteLLM_ManagementEndpoint_MetadataFields_Premium + LiteLLM_ManagementEndpoint_MetadataFields)
|
||||
}
|
||||
if data[_metadata_variable_name].get("user_api_key_auth_metadata") is None:
|
||||
data[_metadata_variable_name]["user_api_key_auth_metadata"] = {}
|
||||
data[_metadata_variable_name]["user_api_key_auth_metadata"].update(added_metadata)
|
||||
|
|
|
|||
|
|
@ -492,6 +492,7 @@ def test_strip_callback_config_drops_credential_bearing_slots():
|
|||
}
|
||||
],
|
||||
"callback_settings": {"callback_vars": {"langfuse_secret_key": "litellm_enc::other"}},
|
||||
"secret_manager_settings": {"vault_token": "vt-secret"},
|
||||
"priority": "high",
|
||||
"guardrails": ["presidio"],
|
||||
"langsmith_provisioning": {"api_key_id": "prov-1"},
|
||||
|
|
@ -501,6 +502,7 @@ def test_strip_callback_config_drops_credential_bearing_slots():
|
|||
|
||||
assert "logging" not in stripped
|
||||
assert "callback_settings" not in stripped
|
||||
assert "secret_manager_settings" not in stripped
|
||||
assert stripped["priority"] == "high"
|
||||
assert stripped["guardrails"] == ["presidio"]
|
||||
assert stripped["langsmith_provisioning"] == {"api_key_id": "prov-1"}
|
||||
|
|
|
|||
|
|
@ -229,6 +229,43 @@ async def test_add_litellm_data_to_request_parses_string_metadata():
|
|||
assert updated_data["metadata"]["generation_name"] == "gen123"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stamped_auth_object_reflects_header_derived_identity():
|
||||
"""
|
||||
Regression (LIT-5487): the stamped object is a copy taken partway through request setup,
|
||||
so it only carries header-derived identity if the stamp still runs after those fields are
|
||||
resolved. Moving the stamp earlier would silently misattribute spend.
|
||||
"""
|
||||
from litellm.proxy.litellm_pre_call_utils import add_litellm_data_to_request
|
||||
|
||||
request_mock = MagicMock(spec=Request)
|
||||
request_mock.url = MagicMock()
|
||||
request_mock.url.path = "/v1/chat/completions"
|
||||
request_mock.url.__str__.return_value = "http://localhost/v1/chat/completions"
|
||||
request_mock.method = "POST"
|
||||
request_mock.query_params = {}
|
||||
request_mock.headers = {"Content-Type": "application/json", "user": "end-user-from-header"}
|
||||
request_mock.client = MagicMock()
|
||||
request_mock.client.host = "127.0.0.1"
|
||||
|
||||
user_api_key_dict = UserAPIKeyAuth(api_key="hashed-key", metadata={}, team_metadata={})
|
||||
|
||||
updated_data = await add_litellm_data_to_request(
|
||||
data={"model": "gpt-3.5-turbo"},
|
||||
request=request_mock,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
proxy_config=MagicMock(),
|
||||
general_settings={"user_header_name": "user"},
|
||||
version="test-version",
|
||||
)
|
||||
|
||||
# precondition: the header was actually resolved onto the live object
|
||||
assert user_api_key_dict.end_user_id == "end-user-from-header"
|
||||
|
||||
stamped = updated_data["metadata"]["user_api_key_auth"]
|
||||
assert stamped.end_user_id == "end-user-from-header"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_litellm_data_to_request_strips_admin_injection_slots():
|
||||
"""User-supplied user_api_key_metadata / user_api_key_team_metadata /
|
||||
|
|
@ -2306,6 +2343,129 @@ def test_add_user_api_key_auth_to_request_metadata():
|
|||
assert result["messages"] == [{"role": "user", "content": "Hello"}]
|
||||
|
||||
|
||||
def _auth_with_callback_credentials() -> UserAPIKeyAuth:
|
||||
return UserAPIKeyAuth(
|
||||
api_key="hashed-test-key-123",
|
||||
key_alias="test-key-alias",
|
||||
team_id="test-team-789",
|
||||
team_alias="test-team-alias",
|
||||
metadata={
|
||||
"logging": [{"callback_name": "langfuse", "callback_vars": {"langfuse_secret_key": "sk-KEY-CANARY"}}],
|
||||
"rpm_limit_type": "guaranteed_throughput",
|
||||
},
|
||||
team_metadata={
|
||||
"callback_settings": {"langfuse": {"callback_vars": {"langfuse_secret_key": "sk-TEAM-CANARY"}}},
|
||||
"secret_manager_settings": {"vault_token": "vt-TEAM-CANARY"},
|
||||
"model_rpm_limit": {"gpt-4": 10},
|
||||
},
|
||||
project_metadata={
|
||||
"logging": [{"callback_vars": {"langfuse_secret_key": "sk-PROJECT-CANARY"}}],
|
||||
"project_tier": "gold",
|
||||
},
|
||||
organization_metadata={
|
||||
"secret_manager_settings": {"vault_token": "vt-ORG-CANARY"},
|
||||
"org_tier": "platinum",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_stamped_auth_object_carries_no_callback_credentials():
|
||||
"""
|
||||
Regression (LIT-5487): the UserAPIKeyAuth stamped into request metadata reaches every
|
||||
raw-metadata logging integration, so it must not carry team/key callback credentials.
|
||||
"""
|
||||
user_api_key_dict = _auth_with_callback_credentials()
|
||||
otel_span = object()
|
||||
user_api_key_dict.parent_otel_span = otel_span
|
||||
user_api_key_dict.budget_reservation = {"amount": 1.0}
|
||||
user_api_key_dict.via_virtual_key = True
|
||||
data = {"litellm_metadata": {}}
|
||||
|
||||
result = LiteLLMProxyRequestSetup.add_user_api_key_auth_to_request_metadata(
|
||||
data=data,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
_metadata_variable_name="litellm_metadata",
|
||||
)
|
||||
|
||||
stamped = result["litellm_metadata"]["user_api_key_auth"]
|
||||
emitted = json.dumps(
|
||||
{
|
||||
"metadata": stamped.metadata,
|
||||
"team_metadata": stamped.team_metadata,
|
||||
"project_metadata": stamped.project_metadata,
|
||||
"organization_metadata": stamped.organization_metadata,
|
||||
},
|
||||
default=str,
|
||||
)
|
||||
assert "sk-KEY-CANARY" not in emitted
|
||||
assert "sk-TEAM-CANARY" not in emitted
|
||||
assert "vt-TEAM-CANARY" not in emitted
|
||||
assert "sk-PROJECT-CANARY" not in emitted
|
||||
assert "vt-ORG-CANARY" not in emitted
|
||||
|
||||
# consumers keep the type and the non-credential slots they read
|
||||
assert isinstance(stamped, UserAPIKeyAuth)
|
||||
assert stamped.key_alias == "test-key-alias"
|
||||
assert stamped.team_id == "test-team-789"
|
||||
assert stamped.team_alias == "test-team-alias"
|
||||
assert stamped.api_key == "hashed-test-key-123"
|
||||
assert stamped.metadata["rpm_limit_type"] == "guaranteed_throughput"
|
||||
assert stamped.team_metadata["model_rpm_limit"] == {"gpt-4": 10}
|
||||
assert stamped.project_metadata["project_tier"] == "gold"
|
||||
assert stamped.organization_metadata["org_tier"] == "platinum"
|
||||
|
||||
# server-only markers are excluded from model_dump, so rebuilding the object
|
||||
# instead of copying it would silently drop them
|
||||
assert stamped.via_virtual_key is True
|
||||
assert stamped.budget_reservation == {"amount": 1.0}
|
||||
assert stamped.parent_otel_span is otel_span
|
||||
|
||||
|
||||
def test_stamping_does_not_mutate_the_cached_auth_object():
|
||||
"""
|
||||
Regression (LIT-5487): UserAPIKeyAuth is cached and model_copy is shallow, so stripping
|
||||
in place would poison the shared dicts and silently kill team callbacks fleet-wide.
|
||||
"""
|
||||
user_api_key_dict = _auth_with_callback_credentials()
|
||||
metadata_before = copy.deepcopy(user_api_key_dict.metadata)
|
||||
team_metadata_before = copy.deepcopy(user_api_key_dict.team_metadata)
|
||||
|
||||
LiteLLMProxyRequestSetup.add_user_api_key_auth_to_request_metadata(
|
||||
data={"litellm_metadata": {}},
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
_metadata_variable_name="litellm_metadata",
|
||||
)
|
||||
|
||||
assert user_api_key_dict.metadata == metadata_before
|
||||
assert user_api_key_dict.team_metadata == team_metadata_before
|
||||
|
||||
|
||||
def test_management_endpoint_metadata_drops_callback_credentials():
|
||||
"""
|
||||
Regression (LIT-5487): user_api_key_auth_metadata is part of StandardLoggingPayload, so a
|
||||
callback_settings-shaped team must not push credentials into it.
|
||||
"""
|
||||
data = {"litellm_metadata": {}}
|
||||
|
||||
result = LiteLLMProxyRequestSetup.add_management_endpoint_metadata_to_request_metadata(
|
||||
data=data,
|
||||
management_endpoint_metadata={
|
||||
"callback_settings": {"langfuse": {"callback_vars": {"langfuse_secret_key": "sk-TEAM-CANARY"}}},
|
||||
"secret_manager_settings": {"vault_token": "vt-TEAM-CANARY"},
|
||||
"logging": [{"callback_vars": {"langfuse_secret_key": "sk-LOGGING-CANARY"}}],
|
||||
"other_field": "value",
|
||||
},
|
||||
_metadata_variable_name="litellm_metadata",
|
||||
)
|
||||
|
||||
auth_metadata = result["litellm_metadata"]["user_api_key_auth_metadata"]
|
||||
emitted = json.dumps(auth_metadata, default=str)
|
||||
assert "sk-TEAM-CANARY" not in emitted
|
||||
assert "vt-TEAM-CANARY" not in emitted
|
||||
assert "sk-LOGGING-CANARY" not in emitted
|
||||
assert auth_metadata["other_field"] == "value"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"data, model_group_settings, expected_headers_added",
|
||||
[
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue