Internal User Endpoint - vulnerability fix + response type fix (#8228)

* fix(key_management_endpoints.py): fix vulnerability where a user could update another user's keys

Resolves https://github.com/BerriAI/litellm/issues/8031

* test(key_management_endpoints.py): return consistent 403 forbidden error when modifying key that doesn't belong to user

* fix(internal_user_endpoints.py): return model max budget in internal user create response

Fixes https://github.com/BerriAI/litellm/issues/7047

* test: fix test

* test: update test to handle gemini token counter change

* fix(factory.py): fix bedrock http:// handling

* docs: fix typo in lm_studio.md (#8222)

* test: fix testing

* test: fix test

---------

Co-authored-by: foreign-sub <51928805+foreign-sub@users.noreply.github.com>
This commit is contained in:
Krish Dholakia 2025-02-04 06:41:14 -08:00 committed by GitHub
parent f6bd48a1c5
commit df93debbc7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 240 additions and 28 deletions

View file

@ -315,3 +315,142 @@ async def test_user_model_access():
key=key,
model="groq/claude-3-5-haiku-20241022",
)
import json
import uuid
import pytest
import aiohttp
from typing import Dict, Tuple
async def setup_test_users(session: aiohttp.ClientSession) -> Tuple[Dict, Dict]:
"""
Create two test users and an additional key for the first user.
Returns tuple of (user1_data, user2_data) where each contains user info and keys.
"""
# Create two test users
user1 = await new_user(
session=session,
i=0,
budget=100,
budget_duration="30d",
models=["anthropic.claude-3-5-sonnet-20240620-v1:0"],
)
user2 = await new_user(
session=session,
i=1,
budget=100,
budget_duration="30d",
models=["anthropic.claude-3-5-sonnet-20240620-v1:0"],
)
print("\nCreated two test users:")
print(f"User 1 ID: {user1['user_id']}")
print(f"User 2 ID: {user2['user_id']}")
# Create an additional key for user1
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {user1['key']}",
}
key_payload = {
"user_id": user1["user_id"],
"duration": "7d",
"key_alias": f"test_key_{uuid.uuid4()}",
"models": ["anthropic.claude-3-5-sonnet-20240620-v1:0"],
}
print("\nGenerating additional key for user1...")
key_response = await session.post(
f"http://0.0.0.0:4000/key/generate", headers=headers, json=key_payload
)
assert key_response.status == 200, "Failed to generate additional key for user1"
user1_additional_key = await key_response.json()
print(f"\nGenerated key details:")
print(json.dumps(user1_additional_key, indent=2))
# Return both users' data including the additional key
return {
"user_data": user1,
"additional_key": user1_additional_key,
"headers": headers,
}, {
"user_data": user2,
"headers": {
"Content-Type": "application/json",
"Authorization": f"Bearer {user2['key']}",
},
}
async def print_response_details(response: aiohttp.ClientResponse) -> None:
"""Helper function to print response details"""
print("\nResponse Details:")
print(f"Status Code: {response.status}")
print("\nResponse Content:")
try:
formatted_json = json.dumps(await response.json(), indent=2)
print(formatted_json)
except json.JSONDecodeError:
print(await response.text())
@pytest.mark.asyncio
async def test_key_update_user_isolation():
"""Test that a user cannot update a key that belongs to another user"""
async with aiohttp.ClientSession() as session:
user1_data, user2_data = await setup_test_users(session)
# Try to update the key to belong to user2
update_payload = {
"key": user1_data["additional_key"]["key"],
"user_id": user2_data["user_data"][
"user_id"
], # Attempting to change ownership
"metadata": {"purpose": "testing_user_isolation", "environment": "test"},
}
print("\nAttempting to update key ownership to user2...")
update_response = await session.post(
f"http://0.0.0.0:4000/key/update",
headers=user1_data["headers"], # Using user1's headers
json=update_payload,
)
await print_response_details(update_response)
# Verify update attempt was rejected
assert (
update_response.status == 403
), "Request should have been rejected with 403 status code"
@pytest.mark.asyncio
async def test_key_delete_user_isolation():
"""Test that a user cannot delete a key that belongs to another user"""
async with aiohttp.ClientSession() as session:
user1_data, user2_data = await setup_test_users(session)
# Try to delete user1's additional key using user2's credentials
delete_payload = {
"keys": [user1_data["additional_key"]["key"]],
}
print("\nAttempting to delete user1's key using user2's credentials...")
delete_response = await session.post(
f"http://0.0.0.0:4000/key/delete",
headers=user2_data["headers"],
json=delete_payload,
)
await print_response_details(delete_response)
# Verify delete attempt was rejected
assert (
delete_response.status == 403
), "Request should have been rejected with 403 status code"