has_users=Users.has_users(db=db)# CHECKrole="admin"ifnothas_userselse...# DECIDE hashed=get_password_hash(password)# bcrypt ~100ms windowuser=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
Start Open WebUI with UVICORN_WORKERS=4 and ENABLE_SIGNUP=true
On a fresh instance (no existing users), fire 20+ concurrent POST requests to /api/v1/auths/signup with different email addresses
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:
Before (vulnerable): Check has_users() → decide role → hash password (~100ms) → insert with pre-decided role
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.
🔄 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>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
📋 Pull Request Information
Original PR: https://github.com/open-webui/open-webui/pull/21630
Author: @theeggorchicken
Created: 2/20/2026
Status: ❌ Closed
Base:
main← Head:fix/signup-race-condition📝 Commits (1)
9f837b0fix: 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-L709What 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
UVICORN_WORKERS=4andENABLE_SIGNUP=true/api/v1/auths/signupwith different email addresses"role": "admin"— without this fix, multiple willFull 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_handlerby reversing the order of operations:has_users()→ decide role → hash password (~100ms) → insert with pre-decided roleget_num_users()after insert → promote to admin only if user count is exactly 1The 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 = Falseguard 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.