diff --git a/litellm/litellm_core_utils/sensitive_data_masker.py b/litellm/litellm_core_utils/sensitive_data_masker.py index 07f652ecb9..985f17e92f 100644 --- a/litellm/litellm_core_utils/sensitive_data_masker.py +++ b/litellm/litellm_core_utils/sensitive_data_masker.py @@ -21,6 +21,8 @@ class SensitiveDataMasker: "access", "private", "certificate", + "fingerprint", + "tenancy", } self.visible_prefix = visible_prefix diff --git a/litellm/proxy/common_utils/openai_endpoint_utils.py b/litellm/proxy/common_utils/openai_endpoint_utils.py index a18ccd0b0c..fa49b05696 100644 --- a/litellm/proxy/common_utils/openai_endpoint_utils.py +++ b/litellm/proxy/common_utils/openai_endpoint_utils.py @@ -6,8 +6,11 @@ from typing import Optional from fastapi import Request +from litellm.litellm_core_utils.sensitive_data_masker import SensitiveDataMasker from litellm.proxy.common_utils.http_parsing_utils import _read_request_body +SENSITIVE_DATA_MASKER = SensitiveDataMasker() + def remove_sensitive_info_from_deployment(deployment_dict: dict) -> dict: """ @@ -25,6 +28,8 @@ def remove_sensitive_info_from_deployment(deployment_dict: dict) -> dict: deployment_dict["litellm_params"].pop("aws_access_key_id", None) deployment_dict["litellm_params"].pop("aws_secret_access_key", None) + deployment_dict["litellm_params"] = SENSITIVE_DATA_MASKER.mask_dict(deployment_dict["litellm_params"]) + return deployment_dict diff --git a/tests/test_litellm/proxy/test_proxy_server.py b/tests/test_litellm/proxy/test_proxy_server.py index d2b516a55d..1cbe6420f6 100644 --- a/tests/test_litellm/proxy/test_proxy_server.py +++ b/tests/test_litellm/proxy/test_proxy_server.py @@ -1886,3 +1886,88 @@ async def test_add_router_settings_shallow_merge_behavior(): assert merged_settings["nested_setting"] == expected_nested assert merged_settings["top_level"] == "db_top" + + +@pytest.mark.asyncio +async def test_model_info_v1_oci_secrets_not_leaked(): + """ + Test that model_info_v1 endpoint properly masks OCI sensitive parameters and does not leak secrets. + """ + from unittest.mock import MagicMock, patch + + from litellm.proxy._types import UserAPIKeyAuth + from litellm.proxy.proxy_server import model_info_v1 + + # Mock user authentication + mock_user_api_key_dict = MagicMock(spec=UserAPIKeyAuth) + mock_user_api_key_dict.user_id = "test-user" + mock_user_api_key_dict.api_key = "test-key" + mock_user_api_key_dict.team_models = [] + mock_user_api_key_dict.models = ["oci-grok-test"] + + # Mock model data with OCI sensitive information + mock_model_data = { + "model_name": "oci-grok-test", + "litellm_params": { + "model": "oci/xai.grok-4", + "oci_key": "ocid1.api_key.oc1..aaaaaaaa7kbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbk", + "oci_region": "us-phoenix-1", + "oci_user": "ocid1.user.oc1..aaaaaaaa7kbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbk", + "oci_fingerprint": "aa:bb:cc:dd:ee:ff:11:22:33:44:55:66:77:88:99:00", + "oci_tenancy": "ocid1.tenancy.oc1..aaaaaaaa7kbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbk", + "oci_key_file": "/path/to/oci_api_key.pem", + "oci_compartment_id": "ocid1.compartment.oc1..aaaaaaaa7kbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbk", + "drop_params": True + }, + "model_info": { + "mode": "completion", + "id": "test-model-id" + } + } + + # Mock the llm_router to return our test data + mock_router = MagicMock() + mock_router.get_model_names.return_value = ["oci-grok-test"] + mock_router.get_model_access_groups.return_value = {} + mock_router.get_model_list.return_value = [mock_model_data] + + # Mock global variables + with patch("litellm.proxy.proxy_server.llm_router", mock_router), \ + patch("litellm.proxy.proxy_server.llm_model_list", [mock_model_data]), \ + patch("litellm.proxy.proxy_server.general_settings", {"infer_model_from_keys": False}), \ + patch("litellm.proxy.proxy_server.user_model", None): + + # Call the model_info_v1 endpoint + result = await model_info_v1( + user_api_key_dict=mock_user_api_key_dict, + litellm_model_id=None + ) + + # Verify the result structure + assert "data" in result + assert len(result["data"]) == 1 + + model_info = result["data"][0] + litellm_params = model_info["litellm_params"] + + # Verify that sensitive OCI fields are masked + assert "****" in litellm_params["oci_key"], "oci_key should be masked" + assert "****" in litellm_params["oci_fingerprint"], "oci_fingerprint should be masked" + assert "****" in litellm_params["oci_tenancy"], "oci_tenancy should be masked" + assert "****" in litellm_params["oci_key_file"], "oci_key_file should be masked" + + # Verify that non-sensitive fields are NOT masked + assert litellm_params["model"] == "oci/xai.grok-4", "model field should not be masked" + assert litellm_params["oci_region"] == "us-phoenix-1", "oci_region should not be masked" + assert litellm_params["drop_params"] is True, "drop_params should not be masked" + + # Verify the model field specifically is not masked (this was the original issue) + assert "****" not in litellm_params["model"], "model field should never be masked" + assert litellm_params["model"].startswith("oci/"), "model should retain its full value" + + # Verify that actual secret values are not present in the response + result_str = str(result) + assert "ocid1.api_key.oc1..aaaaaaaa7kbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbk" not in result_str + assert "aa:bb:cc:dd:ee:ff:11:22:33:44:55:66:77:88:99:00" not in result_str + assert "ocid1.tenancy.oc1..aaaaaaaa7kbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbkbk" not in result_str + assert "/path/to/oci_api_key.pem" not in result_str