Low severityNVD Advisory· Published Sep 4, 2025· Updated Sep 5, 2025
Weblate has long session expiry times during second factor verification
CVE-2025-58352
Description
Weblate is a web based localization tool. Versions lower than 5.13.1 contain a vulnerability that causes long session expiry during the second factor verification. The long session expiry could be used to circumvent rate limiting of the second factor. This issue is fixed in version 5.13.1.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
WeblatePyPI | < 5.13.1 | 5.13.1 |
Affected products
1- Range: < 5.13.1
Patches
10b46fe596231fix(auth): shorten session expiry while in 2fa
9 files changed · +54 −14
docs/admin/config.rst+10 −0 modified@@ -1812,6 +1812,16 @@ Configure sampling rate for profiling monitoring. Set to 1 to trace all events, `Sentry Profiling <https://docs.sentry.io/product/explore/profiling/>`_ +.. setting:: SESSION_COOKIE_AGE_2FA + +SESSION_COOKIE_AGE_2FA +---------------------- + +.. versionadded:: 5.13.1 + +Set session expiry while in :ref:`2fa`. This complements +:setting:`django:SESSION_COOKIE_AGE` which is used for unauthenticated users. + .. setting:: SESSION_COOKIE_AGE_AUTHENTICATED SESSION_COOKIE_AGE_AUTHENTICATED
docs/changes.rst+3 −0 modified@@ -13,6 +13,7 @@ Weblate 5.13.1 * Access control for :http:get:`/api/users/(str:username)/`. * :ref:`file_format_params` were not properly applied in some situations. * :ref:`mt-libretranslate` compatibility with LibreTranslate 1.7.0. +* Shorten session expiry while in :ref:`2fa`. .. rubric:: Compatibility @@ -22,6 +23,8 @@ Weblate 5.13.1 Please follow :ref:`generic-upgrade-instructions` in order to perform update. +* There is a change in :file:`settings_example.py`, ``django_otp.middleware.OTPMiddleware`` was removed from ``MIDDLEWARE``; please adjust your settings accordingly. + .. rubric:: Contributors .. include:: changes/contributors/5.13.1.rst
weblate/accounts/forms.py+7 −7 modified@@ -785,14 +785,14 @@ def clean(self): ) % lockout_period ) - self.user_cache = cast( + user = self.user_cache = cast( "User | None", authenticate(self.request, username=username, password=password), ) - if self.user_cache is None: - for user in try_get_user(username, True): + if user is None: + for failed_user in try_get_user(username, True): audit = AuditLog.objects.create( - user, + failed_user, self.request, "failed-auth", method="password", @@ -803,14 +803,14 @@ def clean(self): raise forms.ValidationError( self.error_messages["invalid_login"], code="invalid_login" ) - if not self.user_cache.is_active or self.user_cache.is_bot: + if not user.is_active or user.is_bot: raise forms.ValidationError( self.error_messages["inactive"], code="inactive" ) AuditLog.objects.create( - self.user_cache, self.request, "login", method="password", name=username + user, self.request, "login", method="password", name=username ) - adjust_session_expiry(self.request) + adjust_session_expiry(request=self.request, user=user) reset_rate_limit("login", self.request) return self.cleaned_data
weblate/accounts/middleware.py+9 −3 modified@@ -11,6 +11,7 @@ from django.contrib.auth.models import AnonymousUser from django.utils.functional import SimpleLazyObject from django.utils.translation import activate, get_language, get_language_from_request +from django_otp.middleware import OTPMiddleware from weblate.accounts.models import set_lang_cookie from weblate.accounts.utils import adjust_session_expiry @@ -35,8 +36,12 @@ def get_user(request: AuthenticatedHttpRequest): return request.weblate_cached_user -class AuthenticationMiddleware: - """Copy of django.contrib.auth.middleware.AuthenticationMiddleware.""" +class AuthenticationMiddleware(OTPMiddleware): + """ + Copy of django.contrib.auth.middleware.AuthenticationMiddleware. + + It subclasses OTPMiddleware to get access to _verify_user. + """ def __init__(self, get_response=None) -> None: self.get_response = get_response @@ -47,6 +52,7 @@ def __call__(self, request: AuthenticatedHttpRequest): # Django uses lazy object here, but we need the user in pretty # much every request, so there is no reason to delay this request.user = user = get_user(request) + self._verify_user(request, user) # Get language to use in this request if user.is_authenticated and user.profile.language: @@ -60,7 +66,7 @@ def __call__(self, request: AuthenticatedHttpRequest): # Extend session expiry for authenticated users if user.is_authenticated: - adjust_session_expiry(request, is_login=False) + adjust_session_expiry(request=request, user=user, is_login=False) # Based on django.middleware.locale.LocaleMiddleware activate(language)
weblate/accounts/pipeline.py+1 −1 modified@@ -455,7 +455,7 @@ def notify_connect( action = "auth-connect" else: action = "login" - adjust_session_expiry(strategy.request) + adjust_session_expiry(request=strategy.request, user=user) AuditLog.objects.create( user, strategy.request,
weblate/accounts/utils.py+14 −1 modified@@ -151,14 +151,22 @@ def cycle_session_keys(request: AuthenticatedHttpRequest, user: User) -> None: def adjust_session_expiry( - request: AuthenticatedHttpRequest, *, is_login: bool = True + *, + request: AuthenticatedHttpRequest, + user: User, + is_login: bool = True, ) -> None: """ Adjust session expiry based on scope. - Set longer expiry for authenticated users. - Set short lived session for SAML authentication flow. """ + is_2fa = False + if user.profile.has_2fa and not user.is_verified(): + # Still in second factor view + is_2fa = True + if "saml_only" not in request.session: if is_login: next_url = request.POST.get("next", request.GET.get("next")) @@ -169,6 +177,11 @@ def adjust_session_expiry( if request.session["saml_only"]: # Short lived session for SAML authentication only request.session.set_expiry(60) + elif is_2fa: + request.session.set_expiry(settings.SESSION_COOKIE_AGE_2FA) + elif is_login: + # Using default expiry for login flow + request.session.set_expiry(settings.SESSION_COOKIE_AGE) else: request.session.set_expiry(settings.SESSION_COOKIE_AGE_AUTHENTICATED)
weblate/auth/models.py+10 −0 modified@@ -62,6 +62,7 @@ if TYPE_CHECKING: from collections.abc import Iterable, Mapping + from django_otp.models import Device from social_core.backends.base import BaseAuth from social_django.models import DjangoStorage from social_django.strategy import DjangoStrategy @@ -506,6 +507,9 @@ class User(AbstractBaseUser): # social_auth integration social_auth: DjangoStorage + # django_otp integration (via OTPMiddleware) + otp_device: Device + EMAIL_FIELD = "email" USERNAME_FIELD = "username" REQUIRED_FIELDS = ["email", "full_name"] @@ -594,6 +598,10 @@ def has_usable_password(self): def is_anonymous(self): return self.username == settings.ANONYMOUS_USER_NAME + def is_verified(self) -> bool: + # django_otp overrides this method in OTPMiddleware + return False + @cached_property def is_authenticated(self) -> bool: # type: ignore[override] return not self.is_anonymous @@ -1253,7 +1261,9 @@ class WeblateAuthConf(AppConf): # Anonymous user name ANONYMOUS_USER_NAME = "anonymous" + SESSION_COOKIE_AGE_AUTHENTICATED = 1209600 + SESSION_COOKIE_AGE_2FA = 180 class Meta: prefix = ""
weblate/settings_docker.py+0 −1 modified@@ -707,7 +707,6 @@ "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "weblate.accounts.middleware.AuthenticationMiddleware", - "django_otp.middleware.OTPMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "social_django.middleware.SocialAuthExceptionMiddleware",
weblate/settings_example.py+0 −1 modified@@ -395,7 +395,6 @@ "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "weblate.accounts.middleware.AuthenticationMiddleware", - "django_otp.middleware.OTPMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "social_django.middleware.SocialAuthExceptionMiddleware",
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-377j-wj38-4728ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-58352ghsaADVISORY
- github.com/WeblateOrg/weblate/commit/0b46fe596231dd456283ead66699ae5516f23908ghsax_refsource_MISCWEB
- github.com/WeblateOrg/weblate/pull/16002ghsax_refsource_MISCWEB
- github.com/WeblateOrg/weblate/security/advisories/GHSA-377j-wj38-4728ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.