From a7f97345b5b0e823a0f50734243ef2e9669f9b8d Mon Sep 17 00:00:00 2001 From: Yucheng Zhu Date: Wed, 15 Jul 2026 15:10:56 -0700 Subject: [PATCH 1/3] fix(key management): enforce minimum custom key length and mask short keys in key_name --- litellm/constants.py | 1 + .../litellm_core_utils/secret_redaction.py | 4 +- litellm/proxy/auth/auth_utils.py | 4 +- .../key_management_endpoints.py | 22 +++- .../proxy/auth/test_auth_utils.py | 10 +- .../test_key_management_endpoints.py | 110 +++++++++++++++++- tests/test_litellm/test_secret_redaction.py | 10 ++ 7 files changed, 151 insertions(+), 10 deletions(-) diff --git a/litellm/constants.py b/litellm/constants.py index 715d57e594d7..21b320d996f1 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -1496,6 +1496,7 @@ MAX_POLICY_ESTIMATE_IMPACT_ROWS = int(os.getenv("MAX_POLICY_ESTIMATE_IMPACT_ROWS", 1000)) DEFAULT_PROMPT_INJECTION_SIMILARITY_THRESHOLD = float(os.getenv("DEFAULT_PROMPT_INJECTION_SIMILARITY_THRESHOLD", 0.7)) LENGTH_OF_LITELLM_GENERATED_KEY = int(os.getenv("LENGTH_OF_LITELLM_GENERATED_KEY", 16)) +MINIMUM_CUSTOM_KEY_LENGTH = int(os.getenv("MINIMUM_CUSTOM_KEY_LENGTH", 20)) SECRET_MANAGER_REFRESH_INTERVAL = int(os.getenv("SECRET_MANAGER_REFRESH_INTERVAL", 86400)) LITELLM_SETTINGS_SAFE_DB_OVERRIDES = [ "default_internal_user_params", diff --git a/litellm/litellm_core_utils/secret_redaction.py b/litellm/litellm_core_utils/secret_redaction.py index b526068589d9..455d0f00c350 100644 --- a/litellm/litellm_core_utils/secret_redaction.py +++ b/litellm/litellm_core_utils/secret_redaction.py @@ -9,6 +9,8 @@ import re from typing import List +from litellm.constants import MINIMUM_CUSTOM_KEY_LENGTH + _REDACTED = "REDACTED" @@ -30,7 +32,7 @@ def _build_secret_patterns() -> "re.Pattern[str]": # Basic auth headers r"Basic\s+[A-Za-z0-9+/]{10,}={0,2}", # OpenAI / Anthropic sk- prefixed keys - r"sk-[A-Za-z0-9\-_]{20,}", + rf"sk-[A-Za-z0-9\-_]{{{MINIMUM_CUSTOM_KEY_LENGTH - len('sk-')},}}", # Generic api_key / api-key / apikey (handles 'key': 'value' dict repr) r"(?:api[_-]?key)['\"]?\s*[:=]\s*['\"]?[^\s,'\"})\]{}>]{8,}", # x-api-key / api-key header values (handles 'key': 'value' dict repr) diff --git a/litellm/proxy/auth/auth_utils.py b/litellm/proxy/auth/auth_utils.py index 893e09ece6e9..a610e44e69cb 100644 --- a/litellm/proxy/auth/auth_utils.py +++ b/litellm/proxy/auth/auth_utils.py @@ -10,7 +10,7 @@ import litellm from litellm import Router, provider_list from litellm._logging import verbose_proxy_logger -from litellm.constants import STANDARD_CUSTOMER_ID_HEADERS +from litellm.constants import MINIMUM_CUSTOM_KEY_LENGTH, STANDARD_CUSTOMER_ID_HEADERS from litellm.litellm_core_utils.safe_json_loads import safe_json_loads from litellm.litellm_core_utils.url_utils import SSRFError, validate_url from litellm.proxy._types import * @@ -1533,4 +1533,6 @@ def get_model_from_request( def abbreviate_api_key(api_key: str) -> str: + if len(api_key) < MINIMUM_CUSTOM_KEY_LENGTH: + return "sk-..." return f"sk-...{api_key[-4:]}" diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index df311bed7b20..354c846d3a6c 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -32,6 +32,7 @@ from litellm.constants import ( LENGTH_OF_LITELLM_GENERATED_KEY, LITELLM_PROXY_ADMIN_NAME, + MINIMUM_CUSTOM_KEY_LENGTH, UI_SESSION_TOKEN_TEAM_ID, ) from litellm.litellm_core_utils.duration_parser import duration_in_seconds @@ -1022,6 +1023,14 @@ async def _common_key_generation_helper( detail={"error": f"Invalid key format. LiteLLM Virtual Key must start with 'sk-'. Received: {_masked}"}, ) + if data.key is not None and len(data.key) < MINIMUM_CUSTOM_KEY_LENGTH: + raise HTTPException( + status_code=400, + detail={ + "error": f"Invalid key format. LiteLLM Virtual Key must be at least {MINIMUM_CUSTOM_KEY_LENGTH} characters long." + }, + ) + # check org key limits - done here to handle inheriting org id from team if data.organization_id is not None: from litellm.proxy.proxy_server import prisma_client, user_api_key_cache @@ -1474,7 +1483,7 @@ async def generate_key_fn( Parameters: - duration: Optional[str] - Specify the length of time the token is valid for. You can set duration as seconds ("30s"), minutes ("30m"), hours ("30h"), days ("30d"). - key_alias: Optional[str] - User defined key alias - - key: Optional[str] - User defined key value. If not set, a 16-digit unique sk-key is created for you. + - key: Optional[str] - User defined key value. Must start with 'sk-' and be at least 20 characters long. If not set, a 16-digit unique sk-key is created for you. - team_id: Optional[str] - The team id of the key - user_id: Optional[str] - The user id of the key - agent_id: Optional[str] - The agent id associated with the key. @@ -1688,7 +1697,7 @@ async def generate_service_account_key_fn( Parameters: - duration: Optional[str] - Specify the length of time the token is valid for. You can set duration as seconds ("30s"), minutes ("30m"), hours ("30h"), days ("30d"). - key_alias: Optional[str] - User defined key alias - - key: Optional[str] - User defined key value. If not set, a 16-digit unique sk-key is created for you. + - key: Optional[str] - User defined key value. Must start with 'sk-' and be at least 20 characters long. If not set, a 16-digit unique sk-key is created for you. - team_id: Optional[str] - The team id of the key - user_id: Optional[str] - [NON-FUNCTIONAL] THIS WILL BE IGNORED. The user id of the key - budget_id: Optional[str] - The budget id associated with the key. Created by calling `/budget/new`. @@ -4364,6 +4373,11 @@ async def get_new_token(data: Optional[RegenerateKeyRequest]) -> str: "error": "New key must start with 'sk-'. This is to distinguish a key hash (used by litellm for logging / internal logic) from the actual key." }, ) + if len(data.new_key) < MINIMUM_CUSTOM_KEY_LENGTH: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail={"error": f"New key must be at least {MINIMUM_CUSTOM_KEY_LENGTH} characters long."}, + ) else: new_token = f"sk-{secrets.token_urlsafe(LENGTH_OF_LITELLM_GENERATED_KEY)}" return new_token @@ -4470,7 +4484,7 @@ async def _execute_virtual_key_regeneration( new_token = await get_new_token(data=data) new_token_hash = hash_token(new_token) - new_token_key_name = f"sk-...{new_token[-4:]}" + new_token_key_name = abbreviate_api_key(api_key=new_token) update_data = {"token": new_token_hash, "key_name": new_token_key_name} non_default_values = {} @@ -4550,7 +4564,7 @@ async def regenerate_key_fn( - data: Optional[RegenerateKeyRequest] - Request body containing optional parameters to update - key: Optional[str] - The key to regenerate. - new_master_key: Optional[str] - The new master key to use, if key is the master key. - - new_key: Optional[str] - The new key to use, if key is not the master key. If both set, new_master_key will be used. + - new_key: Optional[str] - The new key to use, if key is not the master key. Must start with 'sk-' and be at least 20 characters long. If both set, new_master_key will be used. - key_alias: Optional[str] - User-friendly key alias - user_id: Optional[str] - User ID associated with key - team_id: Optional[str] - Team ID associated with key diff --git a/tests/test_litellm/proxy/auth/test_auth_utils.py b/tests/test_litellm/proxy/auth/test_auth_utils.py index 042fc107f40a..33332e435e0c 100644 --- a/tests/test_litellm/proxy/auth/test_auth_utils.py +++ b/tests/test_litellm/proxy/auth/test_auth_utils.py @@ -659,7 +659,15 @@ def test_get_model_from_request_ignores_session_model_on_non_realtime_routes(): def test_abbreviate_api_key(): - assert abbreviate_api_key("sk-test-1234") == "sk-...1234" + assert abbreviate_api_key("sk-test-1234-abcdefgh") == "sk-...efgh" + + +def test_abbreviate_api_key_short_key_is_fully_masked(): + """Regression test for LIT-4355: for keys shorter than the enforced minimum, + showing the last 4 characters can reveal the entire key (sk-1234 -> sk-...1234).""" + assert abbreviate_api_key("sk-1234") == "sk-..." + assert abbreviate_api_key("sk-test-1234") == "sk-..." + assert abbreviate_api_key("") == "sk-..." def test_get_customer_user_header_returns_none_when_no_customer_role(): diff --git a/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py index aa24b0199ab9..1e84caf5cfa9 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py @@ -563,7 +563,7 @@ async def _insert_data_side_effect(*args, **kwargs): generate_key_fn, ) - raw_key = "sk-short-secret" + raw_key = "sk-short-secret-a1b2" with caplog.at_level(logging.DEBUG, logger="LiteLLM Proxy"): await generate_key_fn( data=GenerateKeyRequest(key=raw_key), @@ -1336,10 +1336,10 @@ async def test_get_new_token_with_valid_key(monkeypatch): ) # Test with valid new_key - data = RegenerateKeyRequest(new_key="sk-test123456789") + data = RegenerateKeyRequest(new_key="sk-test1234567890abc") result = await get_new_token(data) - assert result == "sk-test123456789" + assert result == "sk-test1234567890abc" @pytest.mark.asyncio @@ -1370,6 +1370,110 @@ async def test_get_new_token_with_invalid_key(monkeypatch): assert "New key must start with 'sk-'" in str(exc_info.value.detail) +@pytest.mark.asyncio +async def test_get_new_token_rejects_short_new_key(monkeypatch): + """Regression test for LIT-4355: a short custom key like sk-99 must be rejected, + otherwise the stored key_name (sk-...{last 4 chars}) reveals the entire key.""" + from unittest.mock import AsyncMock + + from fastapi import HTTPException + + from litellm.proxy._types import RegenerateKeyRequest + from litellm.proxy.management_endpoints.key_management_endpoints import ( + get_new_token, + ) + + monkeypatch.setattr( + "litellm.proxy.management_endpoints.key_management_endpoints.get_ui_settings_cached", + AsyncMock(return_value={}), + ) + + data = RegenerateKeyRequest(new_key="sk-99") + + with pytest.raises(HTTPException) as exc_info: + await get_new_token(data) + + assert exc_info.value.status_code == 400 + assert "at least 20 characters" in str(exc_info.value.detail) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("short_key", ["sk-1234", "sk-abcdefghijklmnop"]) +async def test_generate_key_fn_rejects_short_custom_key(monkeypatch, short_key): + """Regression test for LIT-4355: /key/generate must reject custom keys shorter + than the minimum length (including the 19-char boundary); sk-1234 used to be + accepted and fully exposed via key_name.""" + mock_prisma_client = AsyncMock() + mock_prisma_client.db = MagicMock() + mock_prisma_client.db.litellm_verificationtoken = MagicMock() + mock_prisma_client.db.litellm_verificationtoken.find_unique = AsyncMock(return_value=None) + mock_prisma_client.db.litellm_verificationtoken.find_many = AsyncMock(return_value=[]) + + from litellm.proxy._types import GenerateKeyRequest, LitellmUserRoles, ProxyException + from litellm.proxy.auth.user_api_key_auth import UserAPIKeyAuth + from litellm.proxy.management_endpoints.key_management_endpoints import ( + generate_key_fn, + ) + + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client) + monkeypatch.setattr( + "litellm.proxy.management_endpoints.key_management_endpoints.get_ui_settings_cached", + AsyncMock(return_value={}), + ) + + assert len(short_key) < 20 + + with pytest.raises(ProxyException) as exc_info: + await generate_key_fn( + data=GenerateKeyRequest(key=short_key), + user_api_key_dict=UserAPIKeyAuth( + user_role=LitellmUserRoles.PROXY_ADMIN, api_key="sk-1234", user_id="1234" + ), + ) + + assert exc_info.value.code == "400" + assert "at least 20 characters" in str(exc_info.value.message) + + +@pytest.mark.asyncio +async def test_generate_key_fn_accepts_custom_key_at_minimum_length(monkeypatch): + """Custom keys at exactly the minimum length (20 chars) are still accepted.""" + mock_prisma_client = AsyncMock() + mock_insert_data = AsyncMock( + return_value=MagicMock(token="hashed_token_123", litellm_budget_table=None, object_permission=None) + ) + mock_prisma_client.insert_data = mock_insert_data + mock_prisma_client.db = MagicMock() + mock_prisma_client.db.litellm_verificationtoken = MagicMock() + mock_prisma_client.db.litellm_verificationtoken.find_unique = AsyncMock(return_value=None) + mock_prisma_client.db.litellm_verificationtoken.find_many = AsyncMock(return_value=[]) + mock_prisma_client.db.litellm_verificationtoken.count = AsyncMock(return_value=0) + + from litellm.proxy._types import GenerateKeyRequest, LitellmUserRoles + from litellm.proxy.auth.user_api_key_auth import UserAPIKeyAuth + from litellm.proxy.management_endpoints.key_management_endpoints import ( + generate_key_fn, + ) + + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client) + monkeypatch.setattr( + "litellm.proxy.management_endpoints.key_management_endpoints.get_ui_settings_cached", + AsyncMock(return_value={}), + ) + + custom_key = "sk-abcdefghijklmnopq" + assert len(custom_key) == 20 + + response = await generate_key_fn( + data=GenerateKeyRequest(key=custom_key), + user_api_key_dict=UserAPIKeyAuth( + user_role=LitellmUserRoles.PROXY_ADMIN, api_key="sk-1234", user_id="1234" + ), + ) + + assert response.key == custom_key + + @pytest.mark.asyncio async def test_check_custom_key_allowed_when_disabled(monkeypatch): """_check_custom_key_allowed raises 403 when disable_custom_api_keys is true.""" diff --git a/tests/test_litellm/test_secret_redaction.py b/tests/test_litellm/test_secret_redaction.py index 85430ba752b7..90c71a906f1b 100644 --- a/tests/test_litellm/test_secret_redaction.py +++ b/tests/test_litellm/test_secret_redaction.py @@ -65,6 +65,16 @@ def test_redact_string_catches_secret_patterns(): assert redact_string(normal) == normal +def test_redact_string_catches_minimum_length_virtual_key(): + """Regression test for LIT-4355: keys at the enforced 20-char minimum + (MINIMUM_CUSTOM_KEY_LENGTH) must be treated as key-shaped by the scrubber.""" + minimum_length_key = "sk-abcdefghijklmnopq" + assert len(minimum_length_key) == 20 + result = redact_string("msg: " + minimum_length_key) + assert minimum_length_key not in result + assert "REDACTED" in result + + def test_filter_redacts_secrets_in_logger_output(): def log_messages(): verbose_logger.debug("Key: " + SECRET) From 09b912244836e36b31ddbe6af62676ae03e8a2aa Mon Sep 17 00:00:00 2001 From: Yucheng Zhu Date: Wed, 15 Jul 2026 15:23:39 -0700 Subject: [PATCH 2/3] fix(key management): validate new_key before assignment and sync generated schema docstrings --- .../management_endpoints/key_management_endpoints.py | 2 +- ui/litellm-dashboard/src/lib/http/schema.d.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index 354c846d3a6c..316116777075 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -4365,7 +4365,6 @@ async def get_new_token(data: Optional[RegenerateKeyRequest]) -> str: if data and data.new_key is not None: # Reject custom key values if disabled by admin await _check_custom_key_allowed(data.new_key) - new_token = data.new_key if not data.new_key.startswith("sk-"): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -4378,6 +4377,7 @@ async def get_new_token(data: Optional[RegenerateKeyRequest]) -> str: status_code=status.HTTP_400_BAD_REQUEST, detail={"error": f"New key must be at least {MINIMUM_CUSTOM_KEY_LENGTH} characters long."}, ) + new_token = data.new_key else: new_token = f"sk-{secrets.token_urlsafe(LENGTH_OF_LITELLM_GENERATED_KEY)}" return new_token diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 9f6d7410e77d..2e3bdc92f455 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -6544,7 +6544,7 @@ export interface paths { * Parameters: * - duration: Optional[str] - Specify the length of time the token is valid for. You can set duration as seconds ("30s"), minutes ("30m"), hours ("30h"), days ("30d"). * - key_alias: Optional[str] - User defined key alias - * - key: Optional[str] - User defined key value. If not set, a 16-digit unique sk-key is created for you. + * - key: Optional[str] - User defined key value. Must start with 'sk-' and be at least 20 characters long. If not set, a 16-digit unique sk-key is created for you. * - team_id: Optional[str] - The team id of the key * - user_id: Optional[str] - The user id of the key * - agent_id: Optional[str] - The agent id associated with the key. @@ -6765,7 +6765,7 @@ export interface paths { * - data: Optional[RegenerateKeyRequest] - Request body containing optional parameters to update * - key: Optional[str] - The key to regenerate. * - new_master_key: Optional[str] - The new master key to use, if key is the master key. - * - new_key: Optional[str] - The new key to use, if key is not the master key. If both set, new_master_key will be used. + * - new_key: Optional[str] - The new key to use, if key is not the master key. Must start with 'sk-' and be at least 20 characters long. If both set, new_master_key will be used. * - key_alias: Optional[str] - User-friendly key alias * - user_id: Optional[str] - User ID associated with key * - team_id: Optional[str] - Team ID associated with key @@ -6834,7 +6834,7 @@ export interface paths { * Parameters: * - duration: Optional[str] - Specify the length of time the token is valid for. You can set duration as seconds ("30s"), minutes ("30m"), hours ("30h"), days ("30d"). * - key_alias: Optional[str] - User defined key alias - * - key: Optional[str] - User defined key value. If not set, a 16-digit unique sk-key is created for you. + * - key: Optional[str] - User defined key value. Must start with 'sk-' and be at least 20 characters long. If not set, a 16-digit unique sk-key is created for you. * - team_id: Optional[str] - The team id of the key * - user_id: Optional[str] - [NON-FUNCTIONAL] THIS WILL BE IGNORED. The user id of the key * - budget_id: Optional[str] - The budget id associated with the key. Created by calling `/budget/new`. @@ -7024,7 +7024,7 @@ export interface paths { * - data: Optional[RegenerateKeyRequest] - Request body containing optional parameters to update * - key: Optional[str] - The key to regenerate. * - new_master_key: Optional[str] - The new master key to use, if key is the master key. - * - new_key: Optional[str] - The new key to use, if key is not the master key. If both set, new_master_key will be used. + * - new_key: Optional[str] - The new key to use, if key is not the master key. Must start with 'sk-' and be at least 20 characters long. If both set, new_master_key will be used. * - key_alias: Optional[str] - User-friendly key alias * - user_id: Optional[str] - User ID associated with key * - team_id: Optional[str] - Team ID associated with key From 9dd04a94547fc4889b13e6637b70a348edce15e8 Mon Sep 17 00:00:00 2001 From: Yucheng Zhu Date: Wed, 15 Jul 2026 16:30:55 -0700 Subject: [PATCH 3/3] fix(key management): lower minimum custom key length default from 20 to 16 --- litellm/constants.py | 2 +- .../key_management_endpoints.py | 6 +++--- tests/test_litellm/proxy/auth/test_auth_utils.py | 1 + .../test_key_management_endpoints.py | 16 ++++++++-------- tests/test_litellm/test_secret_redaction.py | 6 +++--- ui/litellm-dashboard/src/lib/http/schema.d.ts | 8 ++++---- 6 files changed, 20 insertions(+), 19 deletions(-) diff --git a/litellm/constants.py b/litellm/constants.py index 21b320d996f1..8e0a5cfe50fb 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -1496,7 +1496,7 @@ MAX_POLICY_ESTIMATE_IMPACT_ROWS = int(os.getenv("MAX_POLICY_ESTIMATE_IMPACT_ROWS", 1000)) DEFAULT_PROMPT_INJECTION_SIMILARITY_THRESHOLD = float(os.getenv("DEFAULT_PROMPT_INJECTION_SIMILARITY_THRESHOLD", 0.7)) LENGTH_OF_LITELLM_GENERATED_KEY = int(os.getenv("LENGTH_OF_LITELLM_GENERATED_KEY", 16)) -MINIMUM_CUSTOM_KEY_LENGTH = int(os.getenv("MINIMUM_CUSTOM_KEY_LENGTH", 20)) +MINIMUM_CUSTOM_KEY_LENGTH = int(os.getenv("MINIMUM_CUSTOM_KEY_LENGTH", 16)) SECRET_MANAGER_REFRESH_INTERVAL = int(os.getenv("SECRET_MANAGER_REFRESH_INTERVAL", 86400)) LITELLM_SETTINGS_SAFE_DB_OVERRIDES = [ "default_internal_user_params", diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index 316116777075..01f4e040e58b 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -1483,7 +1483,7 @@ async def generate_key_fn( Parameters: - duration: Optional[str] - Specify the length of time the token is valid for. You can set duration as seconds ("30s"), minutes ("30m"), hours ("30h"), days ("30d"). - key_alias: Optional[str] - User defined key alias - - key: Optional[str] - User defined key value. Must start with 'sk-' and be at least 20 characters long. If not set, a 16-digit unique sk-key is created for you. + - key: Optional[str] - User defined key value. Must start with 'sk-' and be at least 16 characters long. If not set, a 16-digit unique sk-key is created for you. - team_id: Optional[str] - The team id of the key - user_id: Optional[str] - The user id of the key - agent_id: Optional[str] - The agent id associated with the key. @@ -1697,7 +1697,7 @@ async def generate_service_account_key_fn( Parameters: - duration: Optional[str] - Specify the length of time the token is valid for. You can set duration as seconds ("30s"), minutes ("30m"), hours ("30h"), days ("30d"). - key_alias: Optional[str] - User defined key alias - - key: Optional[str] - User defined key value. Must start with 'sk-' and be at least 20 characters long. If not set, a 16-digit unique sk-key is created for you. + - key: Optional[str] - User defined key value. Must start with 'sk-' and be at least 16 characters long. If not set, a 16-digit unique sk-key is created for you. - team_id: Optional[str] - The team id of the key - user_id: Optional[str] - [NON-FUNCTIONAL] THIS WILL BE IGNORED. The user id of the key - budget_id: Optional[str] - The budget id associated with the key. Created by calling `/budget/new`. @@ -4564,7 +4564,7 @@ async def regenerate_key_fn( - data: Optional[RegenerateKeyRequest] - Request body containing optional parameters to update - key: Optional[str] - The key to regenerate. - new_master_key: Optional[str] - The new master key to use, if key is the master key. - - new_key: Optional[str] - The new key to use, if key is not the master key. Must start with 'sk-' and be at least 20 characters long. If both set, new_master_key will be used. + - new_key: Optional[str] - The new key to use, if key is not the master key. Must start with 'sk-' and be at least 16 characters long. If both set, new_master_key will be used. - key_alias: Optional[str] - User-friendly key alias - user_id: Optional[str] - User ID associated with key - team_id: Optional[str] - Team ID associated with key diff --git a/tests/test_litellm/proxy/auth/test_auth_utils.py b/tests/test_litellm/proxy/auth/test_auth_utils.py index 33332e435e0c..17ff700791f9 100644 --- a/tests/test_litellm/proxy/auth/test_auth_utils.py +++ b/tests/test_litellm/proxy/auth/test_auth_utils.py @@ -660,6 +660,7 @@ def test_get_model_from_request_ignores_session_model_on_non_realtime_routes(): def test_abbreviate_api_key(): assert abbreviate_api_key("sk-test-1234-abcdefgh") == "sk-...efgh" + assert abbreviate_api_key("sk-abcdefghijklm") == "sk-...jklm" def test_abbreviate_api_key_short_key_is_fully_masked(): diff --git a/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py index 1e84caf5cfa9..dffca3093fa2 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py @@ -1394,14 +1394,14 @@ async def test_get_new_token_rejects_short_new_key(monkeypatch): await get_new_token(data) assert exc_info.value.status_code == 400 - assert "at least 20 characters" in str(exc_info.value.detail) + assert "at least 16 characters" in str(exc_info.value.detail) @pytest.mark.asyncio -@pytest.mark.parametrize("short_key", ["sk-1234", "sk-abcdefghijklmnop"]) +@pytest.mark.parametrize("short_key", ["sk-1234", "sk-abcdefghijkl"]) async def test_generate_key_fn_rejects_short_custom_key(monkeypatch, short_key): """Regression test for LIT-4355: /key/generate must reject custom keys shorter - than the minimum length (including the 19-char boundary); sk-1234 used to be + than the minimum length (including the 15-char boundary); sk-1234 used to be accepted and fully exposed via key_name.""" mock_prisma_client = AsyncMock() mock_prisma_client.db = MagicMock() @@ -1421,7 +1421,7 @@ async def test_generate_key_fn_rejects_short_custom_key(monkeypatch, short_key): AsyncMock(return_value={}), ) - assert len(short_key) < 20 + assert len(short_key) < 16 with pytest.raises(ProxyException) as exc_info: await generate_key_fn( @@ -1432,12 +1432,12 @@ async def test_generate_key_fn_rejects_short_custom_key(monkeypatch, short_key): ) assert exc_info.value.code == "400" - assert "at least 20 characters" in str(exc_info.value.message) + assert "at least 16 characters" in str(exc_info.value.message) @pytest.mark.asyncio async def test_generate_key_fn_accepts_custom_key_at_minimum_length(monkeypatch): - """Custom keys at exactly the minimum length (20 chars) are still accepted.""" + """Custom keys at exactly the minimum length (16 chars) are still accepted.""" mock_prisma_client = AsyncMock() mock_insert_data = AsyncMock( return_value=MagicMock(token="hashed_token_123", litellm_budget_table=None, object_permission=None) @@ -1461,8 +1461,8 @@ async def test_generate_key_fn_accepts_custom_key_at_minimum_length(monkeypatch) AsyncMock(return_value={}), ) - custom_key = "sk-abcdefghijklmnopq" - assert len(custom_key) == 20 + custom_key = "sk-abcdefghijklm" + assert len(custom_key) == 16 response = await generate_key_fn( data=GenerateKeyRequest(key=custom_key), diff --git a/tests/test_litellm/test_secret_redaction.py b/tests/test_litellm/test_secret_redaction.py index 90c71a906f1b..7152eea7c9f1 100644 --- a/tests/test_litellm/test_secret_redaction.py +++ b/tests/test_litellm/test_secret_redaction.py @@ -66,10 +66,10 @@ def test_redact_string_catches_secret_patterns(): def test_redact_string_catches_minimum_length_virtual_key(): - """Regression test for LIT-4355: keys at the enforced 20-char minimum + """Regression test for LIT-4355: keys at the enforced 16-char minimum (MINIMUM_CUSTOM_KEY_LENGTH) must be treated as key-shaped by the scrubber.""" - minimum_length_key = "sk-abcdefghijklmnopq" - assert len(minimum_length_key) == 20 + minimum_length_key = "sk-abcdefghijklm" + assert len(minimum_length_key) == 16 result = redact_string("msg: " + minimum_length_key) assert minimum_length_key not in result assert "REDACTED" in result diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 2e3bdc92f455..7d3fd2869c11 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -6544,7 +6544,7 @@ export interface paths { * Parameters: * - duration: Optional[str] - Specify the length of time the token is valid for. You can set duration as seconds ("30s"), minutes ("30m"), hours ("30h"), days ("30d"). * - key_alias: Optional[str] - User defined key alias - * - key: Optional[str] - User defined key value. Must start with 'sk-' and be at least 20 characters long. If not set, a 16-digit unique sk-key is created for you. + * - key: Optional[str] - User defined key value. Must start with 'sk-' and be at least 16 characters long. If not set, a 16-digit unique sk-key is created for you. * - team_id: Optional[str] - The team id of the key * - user_id: Optional[str] - The user id of the key * - agent_id: Optional[str] - The agent id associated with the key. @@ -6765,7 +6765,7 @@ export interface paths { * - data: Optional[RegenerateKeyRequest] - Request body containing optional parameters to update * - key: Optional[str] - The key to regenerate. * - new_master_key: Optional[str] - The new master key to use, if key is the master key. - * - new_key: Optional[str] - The new key to use, if key is not the master key. Must start with 'sk-' and be at least 20 characters long. If both set, new_master_key will be used. + * - new_key: Optional[str] - The new key to use, if key is not the master key. Must start with 'sk-' and be at least 16 characters long. If both set, new_master_key will be used. * - key_alias: Optional[str] - User-friendly key alias * - user_id: Optional[str] - User ID associated with key * - team_id: Optional[str] - Team ID associated with key @@ -6834,7 +6834,7 @@ export interface paths { * Parameters: * - duration: Optional[str] - Specify the length of time the token is valid for. You can set duration as seconds ("30s"), minutes ("30m"), hours ("30h"), days ("30d"). * - key_alias: Optional[str] - User defined key alias - * - key: Optional[str] - User defined key value. Must start with 'sk-' and be at least 20 characters long. If not set, a 16-digit unique sk-key is created for you. + * - key: Optional[str] - User defined key value. Must start with 'sk-' and be at least 16 characters long. If not set, a 16-digit unique sk-key is created for you. * - team_id: Optional[str] - The team id of the key * - user_id: Optional[str] - [NON-FUNCTIONAL] THIS WILL BE IGNORED. The user id of the key * - budget_id: Optional[str] - The budget id associated with the key. Created by calling `/budget/new`. @@ -7024,7 +7024,7 @@ export interface paths { * - data: Optional[RegenerateKeyRequest] - Request body containing optional parameters to update * - key: Optional[str] - The key to regenerate. * - new_master_key: Optional[str] - The new master key to use, if key is the master key. - * - new_key: Optional[str] - The new key to use, if key is not the master key. Must start with 'sk-' and be at least 20 characters long. If both set, new_master_key will be used. + * - new_key: Optional[str] - The new key to use, if key is not the master key. Must start with 'sk-' and be at least 16 characters long. If both set, new_master_key will be used. * - key_alias: Optional[str] - User-friendly key alias * - user_id: Optional[str] - User ID associated with key * - team_id: Optional[str] - Team ID associated with key