Overview
Security misconfigurations in early Django versions enabled cross-user privilege escalation through a flawed LazyUser implementation in the AuthenticationMiddleware. CVE-2007-0405 documents that Django 0.95’s LazyUser class did not cache the username properly across requests, allowing remote authenticated users to gain the privileges of a different user. In practice, this meant an attacker could leverage a shared or stale cache to present a different identity to request processing, effectively elevating their access within the application. The vulnerability stems from caching user identity in a way that persists beyond the scope of a single HTTP request, violating the per-request isolation that modern authentication mechanisms rely on to tie sessions to the appropriate user context.
Exploitation leveraged by this flaw involves exploiting the per-process or class-level cache used by the LazyUser object. When the first authenticated request cached a username, subsequent requests-potentially from the same process handling multiple clients-could re-use that cached username, resulting in request.user reflecting a different user than intended. This kind of cross-request caching breaches the principal of per-request authentication state, making it possible for an attacker to access resources and privileges associated with another account if the timing and session data align unfavorably.
Remediation for this class of misconfigurations centers on removing cross-request caches and ensuring user identity is derived anew from the current request/session data on every request. Upgrading Django to a version that patches LazyUser handling (or applying the patch) eliminates the shared, per-process caching and aligns with Django’s current authentication model. In addition to upgrading, teams should audit custom authentication logic, ensure request.user is computed per-request, and rely on Django’s maintained AuthenticationMiddleware to enforce per-request user identity. Security testing should verify that identity does not leak across requests and sessions, especially under concurrent processing scenarios.
Affected Versions
Django 0.95 and earlier
Code Fix Example
Django API Security Remediation
import threading
# Minimal stubs to illustrate the vulnerability and fix without a full Django setup
class User:
def __init__(self, username):
self.username = username
@classmethod
def get_by_username(cls, username):
if username in {'alice', 'bob', 'charlie'}:
return cls(username)
return None
@property
def is_authenticated(self):
return True
class AnonymousUser:
is_authenticated = False
username = None
class MockRequest:
def __init__(self, session):
self.session = session
# Vulnerable implementation: caches username at the class level (shared across requests)
class LazyUserVulnerable:
_cached_username = None # shared across requests (vulnerable)
def __init__(self, request):
self.request = request
if LazyUserVulnerable._cached_username is None:
LazyUserVulnerable._cached_username = request.session.get('_auth_user_username')
self.username = LazyUserVulnerable._cached_username
@property
def user(self):
if self.username is None:
return AnonymousUser()
return User(self.username)
@property
def is_authenticated(self):
return self.user.is_authenticated
# Fixed implementation: computes per-request user from session data without shared cache
class LazyUserFixed:
def __init__(self, request):
self.request = request
self._username = request.session.get('_auth_user_username')
self._user = None
@property
def user(self):
if self._user is None:
if self._username is None:
self._user = AnonymousUser()
else:
self._user = User(self._username)
return self._user
@property
def is_authenticated(self):
return self.user.is_authenticated
def main():
# Simulate two separate requests with different sessions
req1 = MockRequest({'_auth_user_username': 'alice'})
req2 = MockRequest({'_auth_user_username': 'bob'})
# Vulnerable behavior: second request reuses cached username from first request
v1 = LazyUserVulnerable(req1)
print('VULN first user:', v1.user.username)
v2 = LazyUserVulnerable(req2)
print('VULN second user (should be bob, but cached as alice):', v2.user.username)
# Fixed behavior: per-request evaluation yields correct usernames
f1 = LazyUserFixed(req1)
print('FIX first user:', f1.user.username)
f2 = LazyUserFixed(req2)
print('FIX second user:', f2.user.username)
if __name__ == '__main__':
main()