VYPR
Critical severityNVD Advisory· Published Feb 10, 2023· Updated Mar 24, 2025

Authentication Bypass by Primary Weakness in modoboa/modoboa

CVE-2023-0777

Description

Authentication Bypass by Primary Weakness in GitHub repository modoboa/modoboa prior to 2.0.4.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Modoboa versions before 2.0.4 allow authentication bypass via weak password reset tokens, enabling account takeover without prior authentication.

Vulnerability

Overview CVE-2023-0777 is an authentication bypass vulnerability in Modoboa, a mail hosting management platform. The root cause is a weak primary key mechanism in the password reset functionality, which allows an attacker to guess or brute-force reset tokens due to insufficient randomness or lack of rate limiting [1]. This bypasses authentication for any account, including administrators.

Exploitation

An attacker can exploit this vulnerability by initiating a password reset request for a known username and then repeatedly submitting different token values until a valid one is found. The attack requires no authentication and minimal preconditions—only the target's email or username. The original implementation did not throttle these attempts, making brute-force feasible [2].

Impact

Successful exploitation grants the attacker full control over the target account, including the ability to change passwords, access administrative functions, and manage mail domains. This can lead to complete compromise of the mail hosting infrastructure [3].

Mitigation

The issue was addressed in Modoboa version 2.0.4, which introduced throttling for the reset password endpoint (via PasswordResetRequestThrottle) and presumably improved token generation [2]. Users are strongly advised to upgrade to 2.0.4 or later. No workarounds have been provided for earlier versions [4].

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
modoboaPyPI
< 2.0.42.0.4

Affected products

2
  • ghsa-coords
    Range: < 2.0.4
  • modoboa/modoboa/modoboav5
    Range: unspecified

Patches

1
47d17ac6643f

Merge pull request #2767 from modoboa/api-throttling

https://github.com/modoboa/modoboaAntoine NguyenFeb 10, 2023via ghsa
22 files changed · +228 25
  • doc/upgrade.rst+32 1 modified
    @@ -123,6 +123,7 @@ The following modifications must be applied to the :file:`settings.py` file:
            },
        ]
     
    +
     * Add the following variable::
     
     .. sourcecode:: python
    @@ -141,10 +142,40 @@ The following modifications must be applied to the :file:`settings.py` file:
        },
     
     
    -You now have the possibility to customize the url of the new-admin
    +* You now have the possibility to customize the url of the new-admin
     interface.  To do so please head up to :ref:`the custom configuration
     chapter <customization>` (advanced user).
     
    +* Add ``DEFAULT_THROTTLE_RATES`` to ``REST_FRAMEWORK``:
    +
    +.. sourcecode:: python
    +
    +   REST_FRAMEWORK = {
    +      'DEFAULT_THROTTLE_RATES': {
    +         'user': '300/minute',
    +         'ddos': '5/second',
    +         'ddos_lesser': '200/minute',
    +         'login': '10/minute',
    +         'password_recovery_request': '12/hour',
    +         'password_recovery_totp_check': '25/hour',
    +         'password_recovery_apply': '25/hour'
    +      },
    +      'DEFAULT_AUTHENTICATION_CLASSES': (
    +         'modoboa.core.drf_authentication.JWTAuthenticationWith2FA',
    +         'rest_framework.authentication.TokenAuthentication',
    +         'rest_framework.authentication.SessionAuthentication',
    +      ),
    +      'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
    +      'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning',
    +   }
    +
    +* You can edit the ``DEFAULT_THROTTLE_RATES`` to whatever value suits you.
    +   - `user` is for every endpoint, it is per user or per ip if not logged.
    +   - `ddos` is per api endpoint and per user or per ip if not logged. 
    +   - `ddos_lesser` is for per api endpoint and per user or per ip if not logged. This is for api endpoint that are lighter.
    +   - `login` the number of time an ip can attempt to log. The counter will reset on login success.
    +   - `password_` is for the recovery, it is divided per step in the recovery process.
    +
     
     2.0.3
     =====
    
  • frontend/src/api/repository.js+7 0 modified
    @@ -3,8 +3,11 @@ import Cookies from 'js-cookie'
     
     import router from '../router'
     import store from '../store'
    +import { translate } from 'vue-gettext'
    +import { bus } from '@/main'
     
     const _axios = axios.create()
    +const { gettext: $gettext } = translate
     
     _axios.interceptors.request.use(
       function (config) {
    @@ -31,6 +34,10 @@ _axios.interceptors.response.use(
           router.push({ name: 'TwoFA' })
           return Promise.reject(error)
         }
    +    if (error.response.status === 429) {
    +      bus.$emit('notification', { msg: $gettext('You are throttled, please try later.'), type: 'error' })
    +      return Promise.reject(error)
    +    }
         if (error.response.status !== 401 || router.currentRoute.path === '/login/') {
           return Promise.reject(error)
         }
    
  • frontend/src/App.vue+5 3 modified
    @@ -60,9 +60,11 @@ export default {
       }),
       methods: {
         showNotification (options) {
    -      this.notification = options.msg
    -      this.notificationColor = (options.type) ? options.type : 'success'
    -      this.snackbar = true
    +      if (this.isAuthenticated) {
    +        this.notification = options.msg
    +        this.notificationColor = (options.type) ? options.type : 'success'
    +        this.snackbar = true
    +      }
         }
       }
     }
    
  • frontend/src/views/Login.vue+4 0 modified
    @@ -94,6 +94,10 @@ export default {
               this.$refs.observer.setErrors({
                 password: this.$gettext('Invalid username and/or password')
               })
    +        } else if (err.response.status === 429) {
    +          this.$refs.observer.setErrors({
    +            password: this.$gettext('Too many unsuccessful attempts, please try later.')
    +          })
             }
           })
         }
    
  • frontend/src/views/user/PasswordRecoveryChangeForm.vue+2 1 modified
    @@ -81,7 +81,6 @@ export default {
           } else {
             const decodedId = atob(this.$route.params.id)
             if (!/^\d+$/.test(decodedId)) {
    -          console.error('Received ID is invalid')
               this.$router.push({ name: 'PasswordRecoveryForm' })
             } else {
               this.id = this.$route.params.id
    @@ -119,6 +118,8 @@ export default {
               err.response.data.errors.forEach(element => {
                 message += this.$gettext(element) + '<br>'
               })
    +        } else if (err.response.status === 429) {
    +          message = this.$gettext('Too many unsuccessful attempts, please try later.')
             }
             this.password_validation_error = message
           })
    
  • frontend/src/views/user/PasswordRecoveryForm.vue+4 0 modified
    @@ -110,6 +110,10 @@ export default {
               }
             } else if (err.response.status === 503 && err.response.data.type === 'email') {
               this.showDialog('Error', err.response.data.reason, true)
    +        } else if (err.response.status === 429) {
    +          this.$refs.observer.setErrors({
    +            email: this.$gettext('Too many unsuccessful attempts, please try later.')
    +          })
             }
           })
         }
    
  • frontend/src/views/user/PasswordRecoverySmsTotpForm.vue+5 1 modified
    @@ -82,13 +82,17 @@ export default {
             this.loading = false
             if (resp.status === 200) {
               this.$refs.observer.setErrors({
    -            password_confirmed: this.$gettext('TOTP resent.')
    +            sms_totp: this.$gettext('TOTP resent.')
               })
             }
           }).catch(err => {
             if (err.response.status === 400) {
               this.loading = false
               this.showErrorDialog(this.$t('User seems wrong, return to login or restart reset the process?'))
    +        } else if (err.response.status === 429) {
    +          this.$refs.observer.setErrors({
    +            sms_totp: this.$gettext('Too many unsuccessful attempts, please try later.')
    +          })
             }
           })
         },
    
  • modoboa/admin/api/v1/viewsets.py+14 5 modified
    @@ -16,6 +16,7 @@
     from modoboa.core import sms_backends
     from modoboa.lib import renderers as lib_renderers
     from modoboa.lib import viewsets as lib_viewsets
    +from modoboa.lib.throttle import GetThrottleViewsetMixin, PasswordResetRequestThrottle
     
     from ... import lib, models
     from . import serializers
    @@ -35,7 +36,7 @@
             summary="Create a new domain"
         )
     )
    -class DomainViewSet(lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet):
    +class DomainViewSet(GetThrottleViewsetMixin, lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet):
         """Domain viewset."""
     
         permission_classes = [IsAuthenticated, DjangoModelPermissions, ]
    @@ -60,7 +61,7 @@ class Meta:
             fields = ["domain"]
     
     
    -class DomainAliasViewSet(lib_viewsets.RevisionModelMixin,
    +class DomainAliasViewSet(GetThrottleViewsetMixin, lib_viewsets.RevisionModelMixin,
                              viewsets.ModelViewSet):
         """ViewSet for DomainAlias."""
     
    @@ -80,13 +81,21 @@ def get_renderer_context(self):
             return context
     
     
    -class AccountViewSet(lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet):
    +class AccountViewSet(GetThrottleViewsetMixin, lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet):
         """ViewSet for User/Mailbox."""
     
         filter_backends = (filters.SearchFilter, )
         permission_classes = [IsAuthenticated, DjangoModelPermissions, ]
         search_fields = ("^first_name", "^last_name", "^email")
     
    +    def get_throttles(self):
    +
    +        throttles = super().get_throttles()
    +        if self.action == "reset_password":
    +            throttles.append(PasswordResetRequestThrottle())
    +        
    +        return throttles
    +
         def get_serializer_class(self):
             """Return a serializer."""
             action_dict = {
    @@ -175,7 +184,7 @@ def reset_password(self, request):
             return Response(body)
     
     
    -class AliasViewSet(lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet):
    +class AliasViewSet(GetThrottleViewsetMixin, lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet):
         """
         create:
         Create a new alias instance.
    @@ -207,7 +216,7 @@ class Meta:
             fields = ["mailbox"]
     
     
    -class SenderAddressViewSet(lib_viewsets.RevisionModelMixin,
    +class SenderAddressViewSet(GetThrottleViewsetMixin, lib_viewsets.RevisionModelMixin,
                                viewsets.ModelViewSet):
         """View set for SenderAddress model."""
     
    
  • modoboa/admin/api/v2/tests.py+1 0 modified
    @@ -224,6 +224,7 @@ def test_create_with_bad_password(self):
             self.assertIn("password", resp.json())
     
         def test_validate(self):
    +        """Test validate and throttling."""
             data = {"username": "toto@test.com"}
             url = reverse("v2:account-validate")
             resp = self.client.post(url, data, format="json")
    
  • modoboa/admin/api/v2/viewsets.py+6 4 modified
    @@ -16,6 +16,7 @@
     from modoboa.core import models as core_models
     from modoboa.lib import renderers as lib_renderers
     from modoboa.lib import viewsets as lib_viewsets
    +from modoboa.lib.throttle import GetThrottleViewsetMixin
     
     from ... import lib
     from ... import models
    @@ -40,7 +41,7 @@
             summary="Delete a particular domain"
         ),
     )
    -class DomainViewSet(lib_viewsets.RevisionModelMixin,
    +class DomainViewSet(GetThrottleViewsetMixin, lib_viewsets.RevisionModelMixin,
                         mixins.ListModelMixin,
                         mixins.RetrieveModelMixin,
                         mixins.CreateModelMixin,
    @@ -160,6 +161,7 @@ class AccountViewSet(v1_viewsets.AccountViewSet):
         filter_backends = (filters.SearchFilter, dj_filters.DjangoFilterBackend)
         filterset_class = AccountFilterSet
     
    +
         def get_serializer_class(self):
             if self.action in ["create", "validate", "update", "partial_update"]:
                 return serializers.WritableAccountSerializer
    @@ -208,7 +210,7 @@ def delete(self, request, **kwargs):
             return response.Response(status=status.HTTP_204_NO_CONTENT)
     
     
    -class IdentityViewSet(viewsets.ViewSet):
    +class IdentityViewSet(GetThrottleViewsetMixin, viewsets.ViewSet):
         """Viewset for identities."""
     
         permission_classes = (permissions.IsAuthenticated, )
    @@ -270,7 +272,7 @@ def random_address(self, request, **kwargs):
             })
     
     
    -class UserAccountViewSet(viewsets.ViewSet):
    +class UserAccountViewSet(GetThrottleViewsetMixin, viewsets.ViewSet):
         """Viewset for current user operations."""
     
         @action(methods=["get", "post"], detail=False)
    @@ -323,7 +325,7 @@ def forward(self, request, **kwargs):
             return response.Response(serializer.validated_data)
     
     
    -class AlarmViewSet(viewsets.ReadOnlyModelViewSet):
    +class AlarmViewSet(GetThrottleViewsetMixin, viewsets.ReadOnlyModelViewSet):
         """Viewset for Alarm."""
     
         filter_backends = (filters.OrderingFilter, filters.SearchFilter, )
    
  • modoboa/core/api/v1/viewsets.py+5 1 modified
    @@ -5,14 +5,18 @@
     import django_otp
     from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
     from django_otp.plugins.otp_totp.models import TOTPDevice
    +
     from rest_framework import permissions, response, viewsets
     from rest_framework.decorators import action
    +
     from drf_spectacular.utils import extend_schema
     
    +from modoboa.lib.throttle import GetThrottleViewsetMixin
    +
     from . import serializers
     
     
    -class AccountViewSet(viewsets.ViewSet):
    +class AccountViewSet(GetThrottleViewsetMixin, viewsets.ViewSet):
         """Account viewset.
     
         Contains endpoints used to manipulate current user's account.
    
  • modoboa/core/api/v2/viewsets.py+3 2 modified
    @@ -17,6 +17,7 @@
     from modoboa.core.api.v1 import serializers as core_v1_serializers
     from modoboa.core.api.v1 import viewsets as core_v1_viewsets
     from modoboa.lib import pagination
    +from modoboa.lib.throttle import GetThrottleViewsetMixin
     
     from ... import constants
     from ... import models
    @@ -148,7 +149,7 @@ def tfa_setup_check(self, request):
             })
     
     
    -class LogViewSet(viewsets.ReadOnlyModelViewSet):
    +class LogViewSet(GetThrottleViewsetMixin, viewsets.ReadOnlyModelViewSet):
         """Log viewset."""
     
         filter_backends = [filters.OrderingFilter, filters.SearchFilter]
    @@ -164,7 +165,7 @@ class LogViewSet(viewsets.ReadOnlyModelViewSet):
         serializer_class = serializers.LogSerializer
     
     
    -class LanguageViewSet(viewsets.ViewSet):
    +class LanguageViewSet(GetThrottleViewsetMixin, viewsets.ViewSet):
         """Language viewset."""
     
         permission_classes = (
    
  • modoboa/core/api/v2/views.py+28 0 modified
    @@ -16,6 +16,7 @@
     
     from modoboa.core.password_hashers import get_password_hasher
     from modoboa.core.utils import check_for_updates
    +from modoboa.lib.throttle import UserLesserDdosUser, LoginThrottle, PasswordResetApplyThrottle, PasswordResetRequestThrottle, PasswordResetTotpThrottle
     from modoboa.parameters import tools as param_tools
     
     from smtplib import SMTPException
    @@ -25,9 +26,20 @@
     logger = logging.getLogger("modoboa.auth")
     
     
    +def delete_cache_key(class_target, throttles, request):
    +    """Attempt to delete cache key from throttling on login/password reset success."""
    +    
    +    for throttle in throttles:
    +        if type(throttle) == class_target:
    +            throttle.reset_cache(request)
    +            return
    +
    +
     class TokenObtainPairView(jwt_views.TokenObtainPairView):
         """We overwrite this view to deal with password scheme update."""
     
    +    throttle_classes = [LoginThrottle]
    +
         def post(self, request, *args, **kwargs):
             serializer = self.get_serializer(data=request.data)
             try:
    @@ -42,6 +54,10 @@ def post(self, request, *args, **kwargs):
     
             user = serializer.user
             login(request, user)
    +
    +        # Reset login throttle
    +        delete_cache_key(LoginThrottle, self.get_throttles(), request)
    +
             logger.info(
                 _("User '%s' successfully logged in"), user.username
             )
    @@ -85,6 +101,8 @@ class EmailPasswordResetView(APIView):
         An Api View which provides a method to request a password reset token based on an e-mail address.
         """
     
    +    throttle_classes = [PasswordResetRequestThrottle]
    +
         def post(self, request, *args, **kwargs):
             serializer = serializers.PasswordRecoveryEmailSerializer(
                 data=request.data, context={'request': request})
    @@ -96,6 +114,7 @@ def post(self, request, *args, **kwargs):
                     "type": "email",
                     "reason": "Error while sending the email. Please contact an administrator."
                 }, 503)
    +
             # Email response
             return response.Response({"type": "email"}, 200)
     
    @@ -114,13 +133,16 @@ def post(self, request, *args, **kwargs):
                 serializer.is_valid(raise_exception=True)
             except serializers.NoSMSAvailable:
                 return super().post(request, *args, **kwargs)
    +        
             # SMS response
             return response.Response({"type": "sms"}, 200)
     
     
     class PasswordResetSmsTOTP(APIView):
         """ Check SMS Totp code. """
     
    +    throttle_classes = [PasswordResetTotpThrottle]
    +
         def post(self, request, *args, **kwargs):
             try:
                 if request.data["type"] == "confirm":
    @@ -140,12 +162,15 @@ def post(self, request, *args, **kwargs):
                     "id": serializer_response[1],
                     "type": "confirm"
                 })
    +        delete_cache_key(PasswordResetTotpThrottle, self.get_throttles(), request)
             return response.Response(payload, 200)
     
     
     class PasswordResetConfirmView(APIView):
         """ Get and set new user password. """
     
    +    throttle_classes = [PasswordResetApplyThrottle]
    +
         def post(self, request, *args, **kwargs):
             serializer = serializers.PasswordRecoveryConfirmSerializer(
                 data=request.data)
    @@ -159,12 +184,15 @@ def post(self, request, *args, **kwargs):
                 data.update({"errors": errors})
                 return response.Response(data, 400)
             serializer.save()
    +        delete_cache_key(PasswordResetApplyThrottle, self.get_throttles(), request)
             return response.Response(status=200)
     
     
     class ComponentsInformationAPIView(APIView):
         """Retrieve information about installed components."""
     
    +    throttle_classes = [UserLesserDdosUser]
    +
         @extend_schema(responses=serializers.ModoboaComponentSerializer(many=True))
         def get(self, request, *args, **kwargs):
             status, extensions = check_for_updates()
    
  • modoboa/core/commands/templates/settings.py.tpl+9 0 modified
    @@ -193,6 +193,15 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
     # Rest framework settings
     
     REST_FRAMEWORK = {
    +    'DEFAULT_THROTTLE_RATES': {
    +        'user': '300/minute',
    +        'ddos': '5/second',
    +        'ddos_lesser': '200/minute',
    +        'login': '10/minute',
    +        'password_recovery_request': '12/hour',
    +        'password_recovery_totp_check': '25/hour',
    +        'password_recovery_apply': '25/hour'
    +    },
         'DEFAULT_AUTHENTICATION_CLASSES': (
             'modoboa.core.drf_authentication.JWTAuthenticationWith2FA',
             'rest_framework.authentication.TokenAuthentication',
    
  • modoboa/dnstools/api/v2/viewsets.py+2 1 modified
    @@ -4,11 +4,12 @@
     from rest_framework.decorators import action
     
     from modoboa.admin import models as admin_models
    +from modoboa.lib.throttle import GetThrottleViewsetMixin
     
     from . import serializers
     
     
    -class DNSViewSet(viewsets.GenericViewSet):
    +class DNSViewSet(GetThrottleViewsetMixin, viewsets.GenericViewSet):
         """A viewset to provide extra routes related to DNS information."""
     
         permission_classes = (permissions.IsAuthenticated, )
    
  • modoboa/lib/throttle.py+79 0 added
    @@ -0,0 +1,79 @@
    +from rest_framework.throttling import SimpleRateThrottle, UserRateThrottle
    +from django.urls import resolve
    +
    +class UserDdosPerView(SimpleRateThrottle):
    +    """Custom Throttle class for rest_framework. The throttling is applied on a per view basis for authentificated users."""
    +
    +    scope = 'ddos'
    +
    +    def get_cache_key(self, request, view):
    +        if request.user and request.user.is_authenticated:
    +            ident = request.user.pk
    +        else:
    +            ident = self.get_ident(request)
    +        return self.cache_format % {
    +            'scope': hash(resolve(request.path).url_name),
    +            'ident': ident
    +        }
    +
    +
    +class UserLesserDdosUser(SimpleRateThrottle):
    +    """Custom Throttle class for rest_framework. The throttling is applied on a per view basis for authentificated users."""
    +
    +    scope = 'ddos_lesser'
    +
    +    def get_cache_key(self, request, view):
    +        if request.user and request.user.is_authenticated:
    +            ident = request.user.pk
    +        else:
    +            ident = self.get_ident(request)
    +        return self.cache_format % {
    +            'scope': hash(resolve(request.path).url_name),
    +            'ident': ident
    +        }
    +
    +
    +class LoginThrottle(SimpleRateThrottle):
    +    """ Custom throttle to reset the cache counter on success. """
    +
    +    scope = 'login'
    +
    +    def get_cache_key(self, request, view):
    +            return self.cache_format % {
    +            'scope': self.scope,
    +            'ident': self.get_ident(request)
    +        }
    +
    +    def reset_cache(self, request):
    +        self.key = self.get_cache_key(request, None)
    +        self.cache.delete(self.key)
    +
    +
    +class PasswordResetRequestThrottle(LoginThrottle):
    +
    +    scope = 'password_recovery_request'
    +
    +
    +class PasswordResetTotpThrottle(LoginThrottle):
    +
    +    scope = 'password_recovery_totp_check'
    +
    +
    +class PasswordResetApplyThrottle(LoginThrottle):
    +
    +    scope = 'password_recovery_apply'
    +
    +
    +class GetThrottleViewsetMixin():
    +    """Override default get_throttle behaviour to assign throttle classes to different actions."""
    +
    +    def get_throttles(self):
    +        """Give lesser_ddos to GET type actions and ddos to others."""
    +
    +        throttles = [UserRateThrottle()]
    +    
    +        if self.action in ["list", "retrieve", "validate", "dns_detail", "me", "dns_detail", "applications", "structure"]:
    +            throttles.append(UserLesserDdosUser())
    +        else:
    +            throttles.append(UserDdosPerView())
    +        return throttles
    \ No newline at end of file
    
  • modoboa/limits/api/v1/viewsets.py+2 1 modified
    @@ -6,10 +6,11 @@
     from rest_framework.permissions import DjangoModelPermissions, IsAuthenticated
     
     from modoboa.core import models as core_models
    +from modoboa.lib.throttle import GetThrottleViewsetMixin
     from . import serializers
     
     
    -class ResourcesViewSet(
    +class ResourcesViewSet(GetThrottleViewsetMixin,
             mixins.RetrieveModelMixin,
             mixins.UpdateModelMixin,
             viewsets.GenericViewSet):
    
  • modoboa/maillog/api/v2/viewsets.py+3 2 modified
    @@ -9,13 +9,14 @@
     
     from modoboa.admin import models as admin_models
     from modoboa.lib import pagination
    +from modoboa.lib.throttle import GetThrottleViewsetMixin
     
     from ... import models
     from ... import signals
     from . import serializers
     
     
    -class StatisticsViewSet(viewsets.ViewSet):
    +class StatisticsViewSet(GetThrottleViewsetMixin,viewsets.ViewSet):
         """A viewset to provide extra route related to mail statistics."""
     
         permission_classes = (permissions.IsAuthenticated, )
    @@ -51,7 +52,7 @@ def list(self, request, **kwargs):
             return response.Response({"graphs": graphs})
     
     
    -class MaillogViewSet(viewsets.ReadOnlyModelViewSet):
    +class MaillogViewSet(GetThrottleViewsetMixin, viewsets.ReadOnlyModelViewSet):
         """Simple viewset to access message log."""
     
         filter_backends = [filters.OrderingFilter, filters.SearchFilter]
    
  • modoboa/parameters/api/v2/viewsets.py+3 1 modified
    @@ -4,11 +4,13 @@
     from rest_framework import response, viewsets
     from rest_framework.decorators import action
     
    +from modoboa.lib.throttle import GetThrottleViewsetMixin
    +
     from . import serializers
     from ... import tools
     
     
    -class ParametersViewSet(viewsets.ViewSet):
    +class ParametersViewSet(GetThrottleViewsetMixin, viewsets.ViewSet):
         """Parameter viewset."""
     
         lookup_value_regex = r"\w+"
    
  • modoboa/relaydomains/api/v1/viewsets.py+2 1 modified
    @@ -4,11 +4,12 @@
     from rest_framework.permissions import DjangoModelPermissions, IsAuthenticated
     
     from modoboa.admin import models as admin_models
    +from modoboa.lib.throttle import GetThrottleViewsetMixin
     from modoboa.lib.viewsets import RevisionModelMixin
     from . import serializers
     
     
    -class RelayDomainViewSet(RevisionModelMixin, viewsets.ModelViewSet):
    +class RelayDomainViewSet(GetThrottleViewsetMixin, RevisionModelMixin, viewsets.ModelViewSet):
         """RelayDomain viewset."""
     
         permission_classes = [IsAuthenticated, DjangoModelPermissions, ]
    
  • modoboa/transport/api/v2/viewsets.py+3 1 modified
    @@ -3,11 +3,13 @@
     from drf_spectacular.utils import extend_schema
     from rest_framework import permissions, response, viewsets
     
    +from modoboa.lib.throttle import GetThrottleViewsetMixin
    +
     from . import serializers
     from ... import backends
     
     
    -class TransportViewSet(viewsets.ViewSet):
    +class TransportViewSet(GetThrottleViewsetMixin, viewsets.ViewSet):
         """Viewset for Transport."""
     
         permissions = (permissions.IsAuthenticated, )
    
  • test_project/test_project/settings.py+9 0 modified
    @@ -186,6 +186,15 @@
     # Rest framework settings
     
     REST_FRAMEWORK = {
    +    'DEFAULT_THROTTLE_RATES': {
    +        'user': '200/minute',
    +        'ddos': '10/second',
    +        'ddos_lesser': '200/minute',
    +        'login': '10/minute',
    +        'password_recovery_request': '11/hour',
    +        'password_recovery_totp_check': '25/hour',
    +        'password_recovery_apply': '25/hour'
    +    },
         'DEFAULT_AUTHENTICATION_CLASSES': (
             'modoboa.core.drf_authentication.JWTAuthenticationWith2FA',
             'rest_framework.authentication.TokenAuthentication',
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.