This document consolidates the following vulnerabilities identified in the social-auth-app-django project. Each vulnerability is detailed below with its description, impact, rank, mitigations, preconditions, source code analysis, and a security test case.
-
Description: The
get_session_timeout
function insocial_django/views.py
calculates the session expiration time based on backend settings (SESSION_EXPIRATION
,MAX_SESSION_LENGTH
) and the expiration time provided by the social authentication provider (social_user.expiration_datetime()
). However, ifSESSION_EXPIRATION
is enabled but the social provider does not return an expiration time (social_user.expiration_datetime()
returnsNone
), andMAX_SESSION_LENGTH
is also not set, the function incorrectly defaults toDEFAULT_SESSION_TIMEOUT
which is explicitly set toNone
. This results in the session being set to platform default session lifetime instead of a more secure, shorter expiration, potentially leading to session fixation or unnecessarily extended session lifetime.Steps to trigger:
- Configure a social authentication backend (e.g., Facebook) in Django settings.
- Enable session expiration by setting
SOCIAL_AUTH_\[BACKEND_NAME]_SESSION_EXPIRATION = True
in Django settings. - Ensure that
SOCIAL_AUTH_\[BACKEND_NAME]_MAX_SESSION_LENGTH
is not set or is set toNone
. - Authenticate a user using the configured social backend.
- If the social authentication provider does not return an expiration time (this behavior depends on the specific provider and its API), the Django session will be set to the platform default lifetime, which might be very long or indefinite.
- An attacker could potentially exploit this by performing a session fixation attack before the user authenticates, or by leveraging an excessively long session lifetime if they gain access to a user's session cookie.
-
Impact: High. If session expiration is not properly enforced to a reasonable timeframe, it increases the risk of session fixation and session hijacking. An attacker could potentially gain unauthorized access to a user's account by exploiting a long-lived session.
-
Vulnerability Rank: high
-
Currently Implemented Mitigations: The code attempts to handle session expiration based on backend settings and provider-supplied expiration. It also includes logic to use
MAX_SESSION_LENGTH
as a maximum session duration. -
Missing Mitigations: The logic in
get_session_timeout
needs to be corrected to enforce a reasonable default session timeout even when the social provider doesn't return an expiration andMAX_SESSION_LENGTH
is not configured. A sensible defaultMAX_SESSION_LENGTH
should be enforced ifSESSION_EXPIRATION
is enabled to prevent excessively long sessions. -
Preconditions:
SESSION_EXPIRATION
setting is enabled for a social backend.MAX_SESSION_LENGTH
is not configured or set toNone
for that backend.- The social authentication provider does not return session expiration information.
-
Source Code Analysis:
# /code/social_django/views.py DEFAULT_SESSION_TIMEOUT = None # Line 15 def get_session_timeout(social_user, enable_session_expiration=False, max_session_length=None): # Line 41 if enable_session_expiration: # Line 42 expiration = social_user.expiration_datetime() # Line 45 if expiration: # Line 49 received_expiration_time = expiration.total_seconds() # Line 50 else: received_expiration_time = DEFAULT_SESSION_TIMEOUT # Line 52 <- Vulnerability: If no provider expiration, defaults to None if received_expiration_time is None and max_session_length is None: # Line 56 session_expiry = DEFAULT_SESSION_TIMEOUT # Line 58 <- Vulnerability: Still None, platform default will be used elif received_expiration_time is None and max_session_length is not None: # Line 59 session_expiry = max_session_length # Line 61 elif received_expiration_time is not None and max_session_length is None: # Line 62 session_expiry = received_expiration_time # Line 64 else: session_expiry = min(received_expiration_time, max_session_length) # Line 67 else: # Line 68 if max_session_length is None: # Line 71 session_expiry = DEFAULT_SESSION_TIMEOUT # Line 73 <- Vulnerability: Still None, platform default will be used if expiration disabled and no max_length else: session_expiry = max_session_length # Line 75 return session_expiry # Line 78 def _do_login(backend, user, social_user): # Line 81 # ... session_expiry = get_session_timeout( # Line 105 social_user, enable_session_expiration=enable_session_expiration, max_session_length=max_session_length, ) try: # Line 111 backend.strategy.request.session.set_expiry(session_expiry) # Line 112 except OverflowError: # Line 113 backend.strategy.request.session.set_expiry(DEFAULT_SESSION_TIMEOUT) # Line 115
The code in
get_session_timeout
function, specifically lines 52 and 58, and lines 73, setssession_expiry
toDEFAULT_SESSION_TIMEOUT
(which isNone
) when either the social provider does not provide an expiration time andMAX_SESSION_LENGTH
is not set, or when session expiration is disabled andmax_session_length
is also not set. This will cause Django to use platform default session expiration, which might be undesirable from a security perspective if a shorter session lifetime is intended. -
Security Test Case:
- Set up Django project with
social-auth-app-django
and configure a social backend (e.g., Facebook, but any backend that doesn't reliably return session expiration is suitable for testing). - In
settings.py
, enable session expiration for the social backend:SOCIAL_AUTH_FACEBOOK_SESSION_EXPIRATION = True # Ensure MAX_SESSION_LENGTH is NOT set for this backend # SOCIAL_AUTH_FACEBOOK_MAX_SESSION_LENGTH = ... # Do not set this
- Log in to the Django application using the configured social backend.
- After successful login, inspect the session cookie in the browser or using developer tools. Check the
Expires/Max-Age
attribute of the session cookie. - If the session cookie's expiration is set to a very long time (e.g., weeks, months, or browser session), it indicates the vulnerability. The expected behavior with
SESSION_EXPIRATION = True
should be a shorter, more controlled session timeout, even if the social provider doesn't provide expiration details. - To further confirm, you can check Django's default session expiration setting in your Django project (e.g.,
SESSION_COOKIE_AGE
insettings.py
). If the observed session expiration matches Django's default, it confirms that the social auth session expiration logic is defaulting to the platform default instead of enforcing a secure timeout.
- Set up Django project with
-
Description: An attacker can potentially force an unintended redirection by triggering an authentication exception. Here’s how the vulnerability can be triggered step by step:
- The social authentication flow uses a middleware—
SocialAuthExceptionMiddleware
—that catches exceptions inheriting fromSocialAuthBaseException
during the authentication process. - When such an exception is caught (for example, by providing an invalid or non‐existent backend during the OAuth callback), the middleware calls its helper method
get_redirect_uri()
. This method simply retrieves a URL from the application’s settings using the keyLOGIN_ERROR_URL
(via the strategy’s settings). - The middleware then calls Django’s built‑in
redirect()
function with the URL obtained. No additional validation is performed on this URL. - If the application’s configuration (i.e. the value of
SOCIAL_AUTH_LOGIN_ERROR_URL
) is misconfigured or is inadvertently set to an absolute URL pointing to an external (or attacker‑controlled) domain rather than a safe relative URL, an attacker can use the authentication error trigger to redirect a user to a malicious site. In summary, by deliberately causing an exception in the social authentication flow (for example, by specifying an invalid backend in the URL), and if the application’s error‑redirect setting is not strictly controlled, the attacker can force a redirect to an arbitrary external URL.
- The social authentication flow uses a middleware—
-
Impact: Exploitation of this vulnerability could lead to phishing attacks or other forms of social engineering. If a user is redirected without warning to a malicious site, they may unknowingly enter sensitive data (such as credentials), thereby compromising their account and potentially the security of the entire application.
-
Vulnerability Rank: High
-
Currently Implemented Mitigations:
- The redirect URL is obtained solely from the server‑side settings (via
strategy.setting("LOGIN_ERROR_URL")
), which in many deployments is configured as a safe, relative URL (as shown in tests such astest_login_error_url
). - Django’s built‑in redirect function is used, which normally works safely when provided with relative URLs.
- The redirect URL is obtained solely from the server‑side settings (via
-
Missing Mitigations:
- There is no explicit validation or sanitization in the middleware to ensure that the URL obtained from
LOGIN_ERROR_URL
is safe (for example, by checking that it is a relative URL or belongs to an allowed whitelist of domains). - In the absence of additional checks, a misconfigured or attacker-influenced setting could cause an open redirect.
- There is no explicit validation or sanitization in the middleware to ensure that the URL obtained from
-
Preconditions:
- The application’s settings for social authentication (specifically,
SOCIAL_AUTH_LOGIN_ERROR_URL
) must be misconfigured to an absolute URL or one that points to an untrusted/external domain. - An attacker must be able to trigger a social authentication exception (for example, by sending a request to the OAuth callback endpoint with an invalid backend name).
- The application’s settings for social authentication (specifically,
-
Source Code Analysis:
- In
social_django/middleware.py
, theprocess_exception()
method is defined to handle any caught exceptions. - When an exception is an instance of
SocialAuthBaseException
, the middleware retrieves the error message and callsself.get_redirect_uri(request, exception)
. - The
get_redirect_uri()
method calls the strategy’s setting method:This passes the configureddef get_redirect_uri(self, request, exception): strategy = getattr(request, "social_strategy", None) return strategy.setting("LOGIN_ERROR_URL")
SOCIAL_AUTH_LOGIN_ERROR_URL
(or its equivalent) directly onward. - Finally, the middleware uses the Django
redirect(url)
function with this URL, appending query parameters (including portions of the exception message) without checking whether the URL is relative (internal) or absolute (external). - As a result, if the setting is an absolute URL, the middleware may cause an open redirect to an attacker-controlled domain.
- In
-
Security Test Case:
- Setup: Configure the application with a misconfigured
SOCIAL_AUTH_LOGIN_ERROR_URL
(for example, set it in your settings or via an override to an external URL such ashttps://malicious.example.com/attack
). - Trigger the Exception:
- Initiate an OAuth callback by sending a GET request to the social authentication complete endpoint (e.g.,
/complete/invalid-backend?code=123&state=abc
). - Since “invalid-backend” is not a recognized social auth provider, the social authentication pipeline will raise a
SocialAuthBaseException
.
- Initiate an OAuth callback by sending a GET request to the social authentication complete endpoint (e.g.,
- Observe the Response:
- The
SocialAuthExceptionMiddleware
catches the exception and retrieves the redirect URL (which comes from the misconfiguredLOGIN_ERROR_URL
). - The response is an HTTP 302 redirect.
- The
- Verification:
- Follow the redirect in your testing tool (for example, using curl or a web browser) and verify that the URL redirects to
https://malicious.example.com/attack
(with additional query parameters appended as determined by the middleware). - This confirms that an attacker—with knowledge of this misconfiguration—can force users to be redirected to an external site.
- Follow the redirect in your testing tool (for example, using curl or a web browser) and verify that the URL redirects to
- Setup: Configure the application with a misconfigured
3. Case-insensitive UID lookup vulnerability in UserSocialAuth.get_social_auth
on case-insensitive databases (CVE-2024-32879)
-
Vulnerability Name: Case-insensitive UID lookup vulnerability in
UserSocialAuth.get_social_auth
on case-insensitive databases (CVE-2024-32879) -
Description: In
social_django/models.py
, theUserSocialAuth.get_social_auth
method performs a case-insensitive lookup for social authentication records when using database backends like MySQL with default settings, or SQLite. This case-insensitivity can be exploited in scenarios where usernames or UIDs differ only in case. An attacker can potentially link their social account to another user's account if they can create a social account entry with a UID that differs only in case from an existing user's social account UID.Steps to trigger:
- Use a database backend that is case-insensitive by default for string comparisons (e.g., MySQL with default collation, SQLite).
- Have two user accounts in the system, say 'User1' and 'user1' (or, more relevantly, social UIDs differing by case).
- User 'Attacker' attempts to link their social account using a social provider.
- During the account linking process, the attacker crafts a social UID that is the same as 'User1's UID but with a different case (if 'User1' has a social account linked).
- Due to the case-insensitive lookup in
UserSocialAuth.get_social_auth
, the attacker's social account might be incorrectly linked to 'User1's account instead of creating a new account or linking to the attacker's intended account.
-
Impact: High. This vulnerability can lead to account takeover or unauthorized account linking. An attacker might be able to link their social login to a victim's account or gain access to a victim's account by exploiting the case-insensitive UID lookup.
-
Vulnerability Rank: high
-
Currently Implemented Mitigations: The vulnerability is mitigated in version 5.4.1 and later by enforcing case-sensitive lookups in
UserSocialAuth.get_social_auth
. The fix involves usingiexact=uid
for case-insensitive databases andexact=uid
for case-sensitive databases, ensuring correct behavior across different database systems. -
Missing Mitigations: Prior to version 5.4.1, there was no explicit handling for case-sensitive vs. case-insensitive database lookups in
UserSocialAuth.get_social_auth
. -
Preconditions:
- The Django application uses
social-auth-app-django
version prior to 5.4.1. - The application is configured to use a database backend that is case-insensitive for lookups by default (e.g., MySQL with default collation, SQLite).
- There exist user accounts or social UIDs in the system that differ only in case.
- The Django application uses
-
Source Code Analysis:
# social_django/models.py (Vulnerable version - before 5.4.1) def get_social_auth(cls, provider, uid): try: return cls.objects.get(provider=provider, uid=uid) # Case-insensitive lookup on some DBs except cls.DoesNotExist: return None
In vulnerable versions,
UserSocialAuth.get_social_auth
usescls.objects.get(provider=provider, uid=uid)
. On case-insensitive databases, theuid=uid
lookup is performed case-insensitively. This can cause incorrectUserSocialAuth
records to be retrieved if multiple records exist with UIDs that are the same when case is ignored.# social_django/models.py (Fixed version - 5.4.1 and later) def get_social_auth(cls, provider, uid): lookup_kwargs = {'provider': provider} if connection.features.has_case_insensitive_like: # Check for case-insensitive DB lookup_kwargs['uid__iexact'] = uid # Use iexact for case-insensitive lookup else: lookup_kwargs['uid'] = uid # Use exact for case-sensitive lookup try: return cls.objects.get(**lookup_kwargs) except cls.DoesNotExist: return None
The fixed version checks
connection.features.has_case_insensitive_like
to determine if the database performs case-insensitive lookups. If it does, it usesuid__iexact=uid
to explicitly perform a case-insensitive lookup (which, in this corrected version, is ironically ensuring correct behavior by being explicitly case-insensitive where needed and case-sensitive where the DB is case-sensitive by default). For case-sensitive databases, it usesuid=uid
for a case-sensitive lookup. This ensures that the lookup is always performed correctly according to the database's case sensitivity behavior. -
Security Test Case:
- Set up a Django project with
social-auth-app-django
version prior to 5.4.1 and configure it to use SQLite or MySQL with default case-insensitive collation. - Create two Django user accounts (e.g., via Django admin or
createsuperuser
). - Manually create
UserSocialAuth
records in the database for each user, using the sameprovider
but UIDs that differ only in case (e.g., 'UserID' and 'userid'). You can use Django's shell (python manage.py shell
) to create these records using theUserSocialAuth
model. - Implement a view that attempts to retrieve a
UserSocialAuth
record usingUserSocialAuth.get_social_auth
with one of the case-variant UIDs. - Test retrieving the
UserSocialAuth
record using different cases for the UID in theget_social_auth
call. - Observe that in vulnerable versions, retrieving with either case might incorrectly return the same
UserSocialAuth
record due to the case-insensitive lookup. - Upgrade
social-auth-app-django
to version 5.4.1 or later and repeat the test. Observe that the lookup now behaves case-sensitively (or explicitly case-insensitively where intended), and retrieving with different cases will yield different or no results as expected. - To simulate an attack, attempt to link a social account using a UID that is a case variant of an existing user's social UID and verify if the account is incorrectly linked.
- Set up a Django project with