From 28f70c75d17815cc9b88963c0825b2c31e1a1853 Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 21 Sep 2026 22:02:05 +0000 Subject: [PATCH] fix(auth): reject unchanged password on /user/password/change Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../password_endpoints.py | 17 +++++++++---- .../test_password_endpoints.py | 25 +++++++++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/litellm/proxy/management_endpoints/password_endpoints.py b/litellm/proxy/management_endpoints/password_endpoints.py index a2466c83ef9..c94399ba25d 100644 --- a/litellm/proxy/management_endpoints/password_endpoints.py +++ b/litellm/proxy/management_endpoints/password_endpoints.py @@ -63,11 +63,12 @@ async def change_password( """ Change the calling user's own password. - Requires the current password. The new password must satisfy the - configured password policy (`general_settings.password_policy_*`: minimum - length, character classes, and, when enabled, breached-password screening - via haveibeenpwned.com). A successful change lifts any pending forced - password reset (`password_reset_required`) on the account. + Requires the current password. The new password must differ from the + current one and satisfy the configured password policy + (`general_settings.password_policy_*`: minimum length, character classes, + and, when enabled, breached-password screening via haveibeenpwned.com). + A successful change lifts any pending forced password reset + (`password_reset_required`) on the account. Parameters: - current_password: str - The user's current password. @@ -103,6 +104,12 @@ async def change_password( if not verify_password(data.current_password, stored_password): raise HTTPException(status_code=400, detail=_error_detail("Current password is incorrect.")) + if data.new_password == data.current_password: + raise HTTPException( + status_code=400, + detail=_error_detail("New password must be different from the current password."), + ) + validate_password_policy(data.new_password, general_settings) await validate_password_not_breached(data.new_password, general_settings) diff --git a/tests/test_litellm/proxy/management_endpoints/test_password_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_password_endpoints.py index bd154ebab41..581a2ef8df8 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_password_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_password_endpoints.py @@ -105,6 +105,31 @@ async def test_change_password_rejects_wrong_current_password(): prisma.db.litellm_usertable.update.assert_not_called() +@pytest.mark.asyncio +async def test_change_password_rejects_unchanged_password(): + from litellm.proxy._types import ChangePasswordRequest + + prisma = _make_prisma(_make_user_row(hash_password(CURRENT_PASSWORD))) + + with ( + patch( # test-quality-ok: change_password reads proxy_server module globals; no injection seam + "litellm.proxy.proxy_server.prisma_client", prisma + ), + patch( # test-quality-ok: change_password reads proxy_server module globals; no injection seam + "litellm.proxy.proxy_server.general_settings", _POLICY_NO_BREACH_CHECK + ), + ): + with pytest.raises(HTTPException) as exc_info: + await change_password( + data=ChangePasswordRequest(current_password=CURRENT_PASSWORD, new_password=CURRENT_PASSWORD), + user_api_key_dict=_caller(), + ) + + assert exc_info.value.status_code == 400 + assert "must be different from the current password" in exc_info.value.detail["error"] + prisma.db.litellm_usertable.update.assert_not_called() + + @pytest.mark.asyncio async def test_change_password_rejects_session_without_user(): from litellm.proxy._types import ChangePasswordRequest