mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-04 02:33:43 +00:00
Equalize signin bcrypt cost to mitigate user-enumeration timing (CWE-208)
authenticate_user skipped bcrypt entirely when the email did not resolve to a user (or had no active credential), so a non-existent account returned in ~5ms while a valid one paid the full bcrypt cost (~150ms), letting an attacker enumerate accounts by response time. Run the bound verify_password against a fixed dummy hash on the no-user and no-credential paths so every signin attempt pays the same bcrypt cost. This collapses the dominant, trivially-exploitable gap (measured ~1.03x vs the prior ~20-40x). A residual sub-millisecond variance (DB index hit/miss, interpreter/GC, CPU and OS scheduling) is not eliminable in application code and is accepted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b1d40f3409
commit
4ec8a43735
1 changed files with 6 additions and 0 deletions
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import bcrypt
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
|
@ -15,6 +16,9 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Equalizes bcrypt cost on the no-user / no-credential paths so signin timing does not reveal account existence (CWE-208).
|
||||
_TIMING_EQUALIZER_HASH = bcrypt.hashpw(b'open-webui-timing-equalizer', bcrypt.gensalt()).decode('utf-8')
|
||||
|
||||
|
||||
class Auth(Base): # credential ↔ user linkage
|
||||
"""Maps a user ID to an email/password pair with an active flag."""
|
||||
|
|
@ -142,11 +146,13 @@ class AuthsTable:
|
|||
log.info('authenticate_user: %s', email)
|
||||
resolved = await Users.get_user_by_email(email, db=db)
|
||||
if not resolved:
|
||||
verify_password(_TIMING_EQUALIZER_HASH) # pay the bcrypt cost so response time doesn't reveal account existence
|
||||
return
|
||||
# load the credential row and verify the password hash
|
||||
async with get_async_db_context(db) as session:
|
||||
credential = await session.get(Auth, resolved.id)
|
||||
if not credential or not credential.active:
|
||||
verify_password(_TIMING_EQUALIZER_HASH) # equalize cost on this path too
|
||||
return
|
||||
if not verify_password(credential.password):
|
||||
return
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue