[PR #21630] [CLOSED] fix: race condition in signup allows multiple admin accounts #49218

Closed
opened 2026-04-30 01:32:40 -05:00 by GiteaMirror · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/open-webui/open-webui/pull/21630
Author: @theeggorchicken
Created: 2/20/2026
Status: Closed

Base: mainHead: fix/signup-race-condition


📝 Commits (1)

  • 9f837b0 fix: race condition in signup allows multiple admin accounts

📊 Changes

1 file changed (+11 additions, -7 deletions)

View changed files

📝 backend/open_webui/routers/auths.py (+11 -7)

📄 Description

Security Fix: TOCTOU Race in Signup Allows Multiple Admin Registrations

Vulnerable Lines

File: backend/open_webui/routers/auths.py#L703-L709

has_users = Users.has_users(db=db)                    # CHECK
role = "admin" if not has_users else ...               # DECIDE  
hashed = get_password_hash(password)                   # bcrypt ~100ms window
user = Auths.insert_new_auth(..., role=role, db=db)   # USE (stale check)

What could happen

On a fresh Open WebUI deployment with multiple uvicorn workers, an attacker can send concurrent signup requests during first-user registration. Each worker's has_users() check returns False before any insert completes, so multiple accounts receive the admin role. I tested this with 4 workers and 30 concurrent requests — 4 accounts got admin. Each had full admin privileges: listing users, reading config, managing the instance.

How to verify

  1. Start Open WebUI with UVICORN_WORKERS=4 and ENABLE_SIGNUP=true
  2. On a fresh instance (no existing users), fire 20+ concurrent POST requests to /api/v1/auths/signup with different email addresses
  3. Check how many responses contain "role": "admin" — without this fix, multiple will

Full HTTP traces and race condition results: evidence gist

What this PR does

Eliminates the TOCTOU (time-of-check-to-time-of-use) race window in signup_handler by reversing the order of operations:

  1. Before (vulnerable): Check has_users() → decide role → hash password (~100ms) → insert with pre-decided role
  2. After (fixed): Hash password → insert with default role → check get_num_users() after insert → promote to admin only if user count is exactly 1

The key insight is that get_num_users() after the insert is a point-in-time read that includes the just-inserted row. If two concurrent requests both insert, both will see count >= 2, so neither gets promoted. Only the truly first user (count == 1) becomes admin.

The fix also moves the ENABLE_SIGNUP = False guard into the same conditional block, so signup is disabled atomically with the admin promotion.

Evidence

https://gist.github.com/theeggorchicken/455f6c46ea73bd96396400a679c3c207


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/open-webui/open-webui/pull/21630 **Author:** [@theeggorchicken](https://github.com/theeggorchicken) **Created:** 2/20/2026 **Status:** ❌ Closed **Base:** `main` ← **Head:** `fix/signup-race-condition` --- ### 📝 Commits (1) - [`9f837b0`](https://github.com/open-webui/open-webui/commit/9f837b0e7b019562d4de9d2952fd752acb11b056) fix: race condition in signup allows multiple admin accounts ### 📊 Changes **1 file changed** (+11 additions, -7 deletions) <details> <summary>View changed files</summary> 📝 `backend/open_webui/routers/auths.py` (+11 -7) </details> ### 📄 Description ## Security Fix: TOCTOU Race in Signup Allows Multiple Admin Registrations ### Vulnerable Lines **File:** [`backend/open_webui/routers/auths.py#L703-L709`](https://github.com/open-webui/open-webui/blob/main/backend/open_webui/routers/auths.py#L703-L709) ```python has_users = Users.has_users(db=db) # CHECK role = "admin" if not has_users else ... # DECIDE hashed = get_password_hash(password) # bcrypt ~100ms window user = Auths.insert_new_auth(..., role=role, db=db) # USE (stale check) ``` ### What could happen On a fresh Open WebUI deployment with multiple uvicorn workers, an attacker can send concurrent signup requests during first-user registration. Each worker's `has_users()` check returns False before any insert completes, so multiple accounts receive the admin role. I tested this with 4 workers and 30 concurrent requests — 4 accounts got admin. Each had full admin privileges: listing users, reading config, managing the instance. ### How to verify 1. Start Open WebUI with `UVICORN_WORKERS=4` and `ENABLE_SIGNUP=true` 2. On a fresh instance (no existing users), fire 20+ concurrent POST requests to `/api/v1/auths/signup` with different email addresses 3. Check how many responses contain `"role": "admin"` — without this fix, multiple will Full HTTP traces and race condition results: [evidence gist](https://gist.github.com/theeggorchicken/455f6c46ea73bd96396400a679c3c207) ### What this PR does Eliminates the TOCTOU (time-of-check-to-time-of-use) race window in `signup_handler` by reversing the order of operations: 1. **Before (vulnerable):** Check `has_users()` → decide role → hash password (~100ms) → insert with pre-decided role 2. **After (fixed):** Hash password → insert with default role → check `get_num_users()` after insert → promote to admin only if user count is exactly 1 The key insight is that `get_num_users()` after the insert is a point-in-time read that includes the just-inserted row. If two concurrent requests both insert, both will see count >= 2, so neither gets promoted. Only the truly first user (count == 1) becomes admin. The fix also moves the `ENABLE_SIGNUP = False` guard into the same conditional block, so signup is disabled atomically with the admin promotion. ### Evidence https://gist.github.com/theeggorchicken/455f6c46ea73bd96396400a679c3c207 --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
GiteaMirror added the pull-request label 2026-04-30 01:32:40 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/open-webui#49218