VYPR
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.

PackageAffected versionsPatched versions
WeblatePyPI
< 5.13.15.13.1

Affected products

1

Patches

1
0b46fe596231

fix(auth): shorten session expiry while in 2fa

https://github.com/WeblateOrg/weblateMichal ČihařSep 1, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.