Sep 16, 2026·5 min read·2 visits
Prior to version 1.0.7, djust does not enforce multi-tenancy boundaries on WebSocket and SSE connection paths. Thread-local context storage is not preserved across ASGI async tasks, leading to an uninitialized tenant ID that defaults to returning unfiltered, global database results to authenticated users.
The multi-tenant isolation mechanism in djust prior to version 1.0.7 fails open on active WebSocket and Server-Sent Events (SSE) connections. Because the tenant context is stored in thread-local variables and initialized exclusively via HTTP middleware, asynchronous event loops executing ASGI/WebSocket code paths do not carry the resolved tenant identifier. When queries are executed without this context, the default database manager fails open, allowing authenticated users of any tenant to query and read sensitive rows across all other tenant accounts.
The djust framework provides high-performance, reactive rendering mechanisms modeled after Phoenix LiveView within Django. To maintain data boundaries in multi-tenant installations, the djust.tenants module filters database QuerySet results using a tenant identifier established during initial requests.
Traditional HTTP requests pass through standard WSGI/ASGI middleware (TenantMiddleware), which resolves and binds the tenant identifier to the current thread. However, live channels such as WebSockets and Server-Sent Events (SSE) operate outside this synchronous request-response flow. This design exposes a significant gap in state initialization for persistent, asynchronous event-driven loops.
On these asynchronous execution paths, the database queries triggered by user-driven live updates do not evaluate against the authorized tenant ID. Instead, the query manager encounters an uninitialized context and defaults to executing unrestricted, global queries. This exposes a direct path to cross-tenant data disclosure.
The root cause of this vulnerability lies in the combination of thread-local context storage and a fail-open query model under asynchronous execution models.
First, djust relied on threading.local() to isolate tenant data per execution thread. In modern ASGI applications, incoming connections are managed inside an asynchronous event loop, with synchronous operations offloaded to a thread pool via Django's sync_to_async wrapper. Because threads in this pool are recycled and shared dynamically across distinct async tasks, the thread-local storage is either entirely empty or polluted with context remnants from completely unrelated execution threads.
Second, the system designed the database manager to fail open when the resolved tenant context was null. Instead of raising an exception or returning an empty set when get_current_tenant() returned None, the QuerySet manager allowed the database query to proceed without filtering. This implementation design meant that any query executing within a thread lacking a tenant context bypassed all isolation constraints, as depicted in the execution flow below:
The vulnerable query management logic in the djust.tenants manager was structured to fallback to an unfiltered parent query when the thread context returned None:
# Insecure implementation prior to v1.0.7
from django.db import models
from threading import local
_thread_locals = local()
def get_current_tenant():
return getattr(_thread_locals, 'tenant_id', None)
class TenantManager(models.Manager):
def get_queryset(self):
tenant_id = get_current_tenant()
if tenant_id is None:
# Fails open: returns all rows across all tenants
return super().get_queryset()
return super().get_queryset().filter(tenant_id=tenant_id)The fix implemented in djust version 1.0.7 migrated context tracking to contextvars.ContextVar, which natively tracks state across asynchronous tasks and coroutines. It also redesigned the manager to fail closed when STRICT_MODE is enabled:
# Remediated implementation in v1.0.7
import contextvars
from django.db import models
from django.core.exceptions import PermissionDenied
from django.conf import settings
# ContextVar safely tracks tenant across asynchronous coroutines
current_tenant_id = contextvars.ContextVar('tenant_id', default=None)
class TenantManager(models.Manager):
def get_queryset(self):
tenant_id = current_tenant_id.get()
if tenant_id is None:
# Fails closed when STRICT_MODE is True
if getattr(settings, 'DJUST_STRICT_MODE', True):
return super().get_queryset().none()
# Log warning if strict mode is disabled
return super().get_queryset()
return super().get_queryset().filter(tenant_id=tenant_id)Exploitation of this vulnerability requires an attacker to possess valid credentials for any single, low-privileged tenant account within the platform. The attacker does not need high-level administrative access or advanced permissions.
First, the attacker authenticates normally through the standard login interface. This populates their session cookie and associated authentication tokens.
Second, the attacker establishes a WebSocket connection or opens an SSE stream to the reactive djust routing endpoint. Because the connection handshake succeeds, the ASGI worker handles subsequent data-fetch events.
Third, when the attacker triggers state synchronizations or parameters updates via WebSocket payloads, the server processes these updates in a thread pool execution task. The worker executes the underlying database queries without establishing the tenant identifier in the local context. The database queries return complete, unfiltered tables. These tables are then serialized and transmitted back through the WebSocket frame to the attacker, exposing cross-tenant records.
The impact of CVE-2026-61595 is severe. In a multi-tenant software-as-a-service application, the absolute isolation of tenant data is a fundamental security contract. A failure in this boundary compromises the confidentiality of all customer records stored in the shared database.
The CVSS v3.1 base score of 7.7 represents a high-severity flaw. The Attack Vector is Network (AV:N), meaning the vulnerability can be reached remotely. Attack Complexity is Low (AC:L) as it is reproducible over standard WebSockets. Privileges Required is Low (PR:L) since any tenant authentication suffices.
The scope is marked as Changed (S:C) because the exploitation of a bug in the reactive rendering layer directly breaks isolation in the logical database access layer. The confidentiality impact is High (C:H) as all tenant tables configured with the flawed manager are readable. Because it is a read-only data disclosure bug on this specific execution path, integrity and availability scores remain None (I:N / A:N).
The recommended remediation is to upgrade djust to version 1.0.7 or later, which shifts state storage to ContextVar and enforces fail-closed query behavior on WebSocket routes.
For systems that cannot be immediately updated, administrators must enforce STRICT_MODE in their Django settings. This forces the database manager to return an empty queryset (.none()) if the tenant context cannot be resolved, converting a data exposure into a service limitation.
Furthermore, developers should run the Django system check framework (python manage.py check) post-upgrade to detect any insecure configurations. This execution validates that STRICT_MODE remains active and that no critical paths bypass context verification.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
djust djust-org | < 1.0.7 | 1.0.7 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-636 / CWE-862 |
| Attack Vector | Network |
| CVSS v3.1 Score | 7.7 |
| Impact | High (Confidentiality) |
| Exploit Status | none |
| KEV Status | Not Listed |
The software is designed to fail in a state that permits access to restricted resources when an unexpected event occurs or context is missing.
CVE-2026-61560 is a critical security vulnerability in the @zereight/mcp-gitlab Server-Sent Events (SSE) server. By utilizing default, unauthenticated route setups and exposing vulnerable administrative tools, remote attackers can execute path traversal attacks to read internal process variables and hijack GitLab operations.
A critical access control vulnerability in djust prior to 1.0.7 exposes diagnostic endpoints and remote method-invocation capabilities to unauthorized network actors. The vulnerability arises due to decoupling IP boundary validation into an opt-in middleware that was omitted from official configuration documentation, leaving views to rely solely on the status of Django's DEBUG flag.
LMDeploy prior to version 0.10.2 is vulnerable to remote code execution because its AsyncRPCServer component implements unauthenticated, remote-accessible communication sockets and uses the insecure pickle.loads() deserializer to process incoming requests.
CVE-2026-68904 is a high-severity Denial of Service (DoS) vulnerability in the node-opcua library. It arises from a logical flaw in the keepalive session manager combined with incorrect socket termination at the TCP transport layer. When server-side anomalies occur, affected clients fall into an infinite, high-frequency reconnection loop. Due to the use of graceful teardown (socket.end) instead of immediate termination (socket.destroy) during negotiation failures, sockets remain open in the FIN-WAIT-2 state. This accumulates system file descriptors and memory, eventually crashing the client process.
CVE-2026-61593 is a high-severity Cross-Site Request Forgery (CSRF) vulnerability discovered in the Server-Sent Events (SSE) transport layer of djust, an open-source framework that implements Phoenix LiveView-style reactive server-side rendering for Django applications. Before version 1.0.7, a lack of origin verification on the SSE stream endpoint, combined with @csrf_exempt decorators on message POST endpoints, allowed an attacker to hijack active client sessions through cross-origin interactions.
An untrusted search path vulnerability (CWE-426) in the OpenTelemetry.Resources.Host NuGet package on macOS allows a local attacker to execute arbitrary code with elevated privileges by hijacking standard system commands such as sh and ioreg.