fix(auth): reject unchanged password on /user/password/change

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
ryan 2026-09-21 22:02:05 +00:00
parent 7c8aed072f
commit 28f70c75d1
2 changed files with 37 additions and 5 deletions

View file

@ -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)

View file

@ -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