diff --git a/litellm/llms/gdc/chat/transformation.py b/litellm/llms/gdc/chat/transformation.py index 61631920a64..dd179b30fc6 100644 --- a/litellm/llms/gdc/chat/transformation.py +++ b/litellm/llms/gdc/chat/transformation.py @@ -155,9 +155,28 @@ class GDCGeminiConfig(OpenAILikeChatConfig): return token - def _load_creds_from_key(self, api_key: str) -> tuple[Any, bool]: + def _allow_local_file_access(self) -> bool: + """Whether a caller-supplied service account file path in api_key may be opened. + + Gated strictly on the ``GDC_ALLOW_LOCAL_FILE_ACCESS`` environment variable. + """ + return self._read_env_bool(None, "GDC_ALLOW_LOCAL_FILE_ACCESS", default=False) + + def _load_creds_from_key(self, api_key: str, model: str | None = None) -> tuple[Any, bool]: + """Helper to safely parse service account JSON files or strings to reduce complexity.""" import google.auth + # Security hardening: Prevent local file inclusion (LFI) via api_key by default. + # Only allow opening local file paths if explicitly enabled via GDC_ALLOW_LOCAL_FILE_ACCESS env var. + allow_local_file = self._allow_local_file_access() + + # Limit length to avoid OSError for 'File name too long' + if allow_local_file and len(api_key) < 2000 and os.path.exists(api_key): + with open(api_key, "r") as f: + json_obj = json.load(f) + creds, _ = google.auth.load_credentials_from_dict(json_obj) + return creds, True + try: json_obj = json.loads(api_key) except json.JSONDecodeError: diff --git a/tests/test_litellm/llms/gdc/chat/test_gdc_chat_transformation.py b/tests/test_litellm/llms/gdc/chat/test_gdc_chat_transformation.py index d106cf7ea21..799fd374212 100644 --- a/tests/test_litellm/llms/gdc/chat/test_gdc_chat_transformation.py +++ b/tests/test_litellm/llms/gdc/chat/test_gdc_chat_transformation.py @@ -715,3 +715,25 @@ class TestCompleteGDC: _, kwargs = mock_completion.call_args assert kwargs["api_base"] == "https://gdc-specific.com" + + @patch("google.auth.load_credentials_from_dict") + @patch("builtins.open", new_callable=MagicMock) + @patch("os.path.exists", return_value=True) + def test_load_creds_from_key_local_file_safety(self, mock_exists, mock_open, mock_load_creds): + config = GDCGeminiConfig() + fake_path = "/tmp/fake_service_account.json" + + # 1. By default, local file access must be blocked (prevents LFI via api_key) + mock_open.reset_mock() + creds, is_sa = config._load_creds_from_key(fake_path, TEST_MODEL) + mock_open.assert_not_called() + + # 2. When enabled via GDC_ALLOW_LOCAL_FILE_ACCESS env var, file access should be allowed + with patch.dict(os.environ, {"GDC_ALLOW_LOCAL_FILE_ACCESS": "true"}): + mock_open.reset_mock() + mock_open.return_value.__enter__.return_value.read.return_value = '{"type": "service_account"}' + mock_load_creds.return_value = (MagicMock(), None) + creds, is_sa = config._load_creds_from_key(fake_path, TEST_MODEL) + mock_open.assert_called_once_with(fake_path, "r") + assert creds is not None + assert is_sa is True