VYPR
Critical severityNVD Advisory· Published Nov 24, 2022· Updated Apr 25, 2025

Improper Restriction of Excessive Authentication Attempts in wger-project/wger

CVE-2022-2650

Description

Improper Restriction of Excessive Authentication Attempts in GitHub repository wger-project/wger prior to 2.2.

AI Insight

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

The wger fitness tracker before version 2.2 lacked proper rate limiting on authentication attempts, enabling brute-force attacks.

Vulnerability

CVE-2022-2650 is an improper restriction of excessive authentication attempts in the wger fitness tracker application prior to version 2.2 [2]. The software failed to implement adequate rate limiting, allowing an attacker to make unlimited login attempts.

Exploitation

An attacker can exploit this by sending a high volume of login requests to the application's authentication endpoint. No special privileges are required; the attacker only needs network access to the wger instance. The lack of lockout or throttling mechanisms allows rapid-fire brute-force attacks [4].

Impact

Successful exploitation could lead to account takeover, as an attacker could guess weak passwords through brute forcing. This compromises user data and potentially the entire application if administrative accounts are targeted.

Mitigation

The issue is fixed in wger version 2.2. Users are advised to update to the latest version. No workarounds are documented, but enabling multi-factor authentication can reduce risk [3].

AI Insight generated on May 21, 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
wgerPyPI
< 2.22.2

Affected products

2
  • ghsa-coords
    Range: < 2.2
  • wger-project/wger-project/wgerv5
    Range: unspecified

Patches

1
5e3167e3a2dc

Merge branch 'master' into add-django-axes

https://github.com/wger-project/wgerRoland GeiderOct 13, 2022via ghsa
10 files changed · +172 31
  • extras/docker/development/settings.py+8 1 modified
    @@ -44,7 +44,7 @@
     TIME_ZONE = env.str("TIME_ZONE", 'Europe/Berlin')
     
     # Make this unique, and don't share it with anybody.
    -SECRET_KEY = env.str("SECRET_KEY", 'wger-django-secret-key')
    +SECRET_KEY = env.str("SECRET_KEY", 'wger-docker-supersecret-key-1234567890!@#$%^&*(-_)')
     
     
     # Your reCaptcha keys
    @@ -122,3 +122,10 @@
     AXES_FAILURE_LIMIT = 5  # configurable, default is 5
     AXES_COOLOFF_TIME = 0.5  # configurable, default is 0.5 hours
     AXES_HANDLER = 'axes.handlers.cache.AxesCacheHandler'  # Configurable, but default is the cache handler
    +
    +#
    +# Django Rest Framework SimpleJWT
    +#
    +SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'] = timedelta(minutes=env.int("ACCESS_TOKEN_LIFETIME", 15))
    +SIMPLE_JWT['REFRESH_TOKEN_LIFETIME'] = timedelta(hours=env.int("REFRESH_TOKEN_LIFETIME", 24))
    +SIMPLE_JWT['SIGNING_KEY'] = env.str("SIGNING_KEY", SECRET_KEY)
    \ No newline at end of file
    
  • .github/workflows/docker-base.yml+3 3 modified
    @@ -17,10 +17,10 @@ jobs:
             uses: actions/checkout@v3
     
           - name: Set up QEMU
    -        uses: docker/setup-qemu-action@v2.0.0
    +        uses: docker/setup-qemu-action@v2.1.0
     
           - name: Set up Docker Buildx
    -        uses: docker/setup-buildx-action@v2.0.0
    +        uses: docker/setup-buildx-action@v2.1.0
     
           - name: Login to DockerHub
             uses: docker/login-action@v2.0.0
    @@ -29,7 +29,7 @@ jobs:
               password: ${{ secrets.DOCKERHUB_TOKEN }}
     
           - name: Build base image
    -        uses: docker/build-push-action@v3.1.1
    +        uses: docker/build-push-action@v3.2.0
             with:
               context: .
               push: true
    
  • .github/workflows/docker.yml+29 9 modified
    @@ -6,38 +6,58 @@ on:
           - master
     
     jobs:
    -  path-context:
    +  apache:
    +    name: Build apache image
         runs-on: ubuntu-latest
         steps:
           - name: Checkout
             uses: actions/checkout@v3
     
           - name: Set up QEMU
    -        uses: docker/setup-qemu-action@v2.0.0
    +        uses: docker/setup-qemu-action@v2.1.0
     
           - name: Set up Docker Buildx
    -        uses: docker/setup-buildx-action@v2.0.0
    +        uses: docker/setup-buildx-action@v2.1.0
     
           - name: Login to DockerHub
             uses: docker/login-action@v2.0.0
             with:
               username: ${{ secrets.DOCKERHUB_USERNAME }}
               password: ${{ secrets.DOCKERHUB_TOKEN }}
     
    -      - name: Build apache image
    -        uses: docker/build-push-action@v3.1.1
    +      - name: Build image
    +        uses: docker/build-push-action@v3.2.0
             with:
               context: .
               push: true
               file: extras/docker/demo/Dockerfile
               platforms: linux/amd64,linux/arm64
    -          tags: wger/demo:latest,wger/demo:2.1-dev,wger/apache:latest,wger/apache:2.1-dev
    +          tags: wger/demo:latest,wger/demo:2.2-dev,wger/apache:latest,wger/apache:2.2-dev
     
    -      - name: Build dev image
    -        uses: docker/build-push-action@v3.1.1
    +  prod:
    +    name: Build production image
    +    runs-on: ubuntu-latest
    +    steps:
    +      - name: Checkout
    +        uses: actions/checkout@v3
    +
    +      - name: Set up QEMU
    +        uses: docker/setup-qemu-action@v2.1.0
    +
    +      - name: Set up Docker Buildx
    +        uses: docker/setup-buildx-action@v2.1.0
    +
    +      - name: Login to DockerHub
    +        uses: docker/login-action@v2.0.0
    +        with:
    +          username: ${{ secrets.DOCKERHUB_USERNAME }}
    +          password: ${{ secrets.DOCKERHUB_TOKEN }}
    +
    +      - name: Build image
    +        uses: docker/build-push-action@v3.2.0
             with:
               context: .
               push: true
               file: extras/docker/development/Dockerfile
               platforms: linux/amd64,linux/arm64
    -          tags: wger/server:latest,wger/server:2.1-dev,wger/devel:latest,wger/devel:2.1-dev
    +          tags: wger/server:latest,wger/server:2.2-dev,wger/devel:latest,wger/devel:2.2-dev
    
  • requirements.txt+3 2 modified
    @@ -12,13 +12,13 @@ django-activity-stream~=1.4
     django-axes==5.39.0
     django-crispy-forms~=1.14
     django-simple-history~=3.1
    -django-email-verification~=0.1.0
    +django-email-verification~=0.3.1
     django_compressor~=4.1
     django_extensions~=3.2
     django-storages~=1.13
     django-environ==0.9.0
     easy-thumbnails==2.8.3
    -fontawesomefree~=6.1.1
    +fontawesomefree~=6.2.0
     icalendar==4.1.0
     invoke==1.7.3
     pillow==9.2.0
    @@ -33,6 +33,7 @@ requests==2.28.1
     django-cors-headers==3.13.0
     django-filter==22.1
     djangorestframework~=3.14
    +djangorestframework-simplejwt[crypto]==5.2.1
     
     # Not used anymore, but needed because some modules are imported in DB migration
     # files
    
  • wger/core/api/views.py+20 2 modified
    @@ -198,14 +198,23 @@ def get(request):
     class UserAPILoginView(viewsets.ViewSet):
         """
         API endpoint for api user objects
    +    .. warning:: This endpoint is deprecated
         """
         permission_classes = (AllowAny, )
         queryset = User.objects.all()
         serializer_class = UserApiSerializer
         throttle_scope = 'login'
     
         def get(self, request):
    -        return Response({'message': "You must send a 'username' and 'password' via POST"})
    +        return Response(
    +            data={
    +                'message': "You must send a 'username' and 'password' via POST",
    +                'warning': "This endpoint is deprecated."
    +            },
    +            headers={
    +                "Deprecation": "Sat, 01 Oct 2022 23:59:59 GMT",
    +            },
    +        )
     
         def post(self, request):
             data = request.data
    @@ -223,7 +232,16 @@ def post(self, request):
                 )
     
             token = create_token(form.get_user())
    -        return Response({'token': token.key}, status=status.HTTP_200_OK)
    +        return Response(
    +            data={
    +                'token': token.key,
    +                'message': "This endpoint is deprecated."
    +            },
    +            status=status.HTTP_200_OK,
    +            headers={
    +                "Deprecation": "Sat, 01 Oct 2022 23:59:59 GMT",
    +            }
    +        )
     
     
     class UserAPIRegistrationViewSet(viewsets.ViewSet):
    
  • wger/exercises/models/exercise.py+6 7 modified
    @@ -116,13 +116,12 @@ def get_absolute_url(self):
             """
             Returns the canonical URL to view an exercise
             """
    -        return reverse(
    -            'exercise:exercise:view-base',
    -            kwargs={
    -                'pk': self.exercise_base_id,
    -                'slug': slugify(self.name)
    -            }
    -        )
    +        slug_name = slugify(self.name)
    +        kwargs = {'pk': self.exercise_base_id}
    +        if slug_name:
    +            kwargs['slug'] = slug_name
    +
    +        return reverse('exercise:exercise:view-base', kwargs=kwargs)
     
         def save(self, *args, **kwargs):
             """
    
  • wger/exercises/tests/test_exercise_model.py+38 0 added
    @@ -0,0 +1,38 @@
    +# This file is part of wger Workout Manager.
    +#
    +# wger Workout Manager is free software: you can redistribute it and/or modify
    +# it under the terms of the GNU Affero General Public License as published by
    +# the Free Software Foundation, either version 3 of the License, or
    +# (at your option) any later version.
    +#
    +# wger Workout Manager is distributed in the hope that it will be useful,
    +# but WITHOUT ANY WARRANTY; without even the implied warranty of
    +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    +# GNU General Public License for more details.
    +#
    +# You should have received a copy of the GNU Affero General Public License
    +
    +# wger
    +from wger.core.tests.base_testcase import WgerTestCase
    +from wger.exercises.models import Exercise
    +
    +
    +class ExerciseModelTestCase(WgerTestCase):
    +    """
    +    Test the logic in the exercise model
    +    """
    +
    +    def test_absolute_url_name(self):
    +        """Test that the get_absolute_url returns the correct URL"""
    +        exercise = Exercise(exercise_base_id=1, description='abc', name='foo')
    +        self.assertEqual(exercise.get_absolute_url(), '/en/exercise/1/view-base/foo')
    +
    +    def test_absolute_url_no_name(self):
    +        """Test that the get_absolute_url returns the correct URL"""
    +        exercise = Exercise(exercise_base_id=2, description='abc', name='')
    +        self.assertEqual(exercise.get_absolute_url(), '/en/exercise/2/view-base')
    +
    +    def test_absolute_url_no_name2(self):
    +        """Test that the get_absolute_url returns the correct URL"""
    +        exercise = Exercise(exercise_base_id=42, description='abc', name='@@@@@')
    +        self.assertEqual(exercise.get_absolute_url(), '/en/exercise/42/view-base')
    
  • wger/settings_global.py+16 2 modified
    @@ -18,6 +18,7 @@
     # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
     import os
     import re
    +from datetime import timedelta
     
     
     """
    @@ -82,6 +83,7 @@
         'rest_framework',
         'rest_framework.authtoken',
         'django_filters',
    +    'rest_framework_simplejwt',
     
         # Breadcrumbs
         'django_bootstrap_breadcrumbs',
    @@ -418,6 +420,7 @@
         'DEFAULT_AUTHENTICATION_CLASSES': (
             'rest_framework.authentication.SessionAuthentication',
             'rest_framework.authentication.TokenAuthentication',
    +        'rest_framework_simplejwt.authentication.JWTAuthentication',
         ),
         'DEFAULT_FILTER_BACKENDS': (
             'django_filters.rest_framework.DjangoFilterBackend',
    @@ -429,6 +432,17 @@
         }
     }
     
    +#
    +# Django Rest Framework SimpleJWT
    +#
    +SIMPLE_JWT = {
    +    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
    +    'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
    +    'ROTATE_REFRESH_TOKENS': False,
    +    'BLACKLIST_AFTER_ROTATION': False,
    +    'UPDATE_LAST_LOGIN': False,
    +}
    +
     #
     # CORS headers: allow all hosts to access the API
     #
    @@ -489,8 +503,8 @@ def email_verified_callback(user):
     EMAIL_MAIL_SUBJECT = 'Confirm your email'
     EMAIL_MAIL_HTML = 'email_verification/email_body_html.tpl'
     EMAIL_MAIL_PLAIN = 'email_verification/email_body_txt.tpl'
    -EMAIL_TOKEN_LIFE = 60 * 60
    -EMAIL_PAGE_TEMPLATE = 'email_verification/confirm_template.html'
    +EMAIL_MAIL_TOKEN_LIFE = 60 * 60
    +EMAIL_MAIL_PAGE_TEMPLATE = 'email_verification/confirm_template.html'
     EMAIL_PAGE_DOMAIN = 'http://localhost:8000/'
     
     #
    
  • wger/software/templates/api.html+39 3 modified
    @@ -16,11 +16,47 @@ <h3>Authentication</h3>
     objects such as workouts, you need to generate an API KEY</strong> and pass
     it in the header, see the link on the sidebar for details.</p>
     
    -<p>You can also generate a token via the <code>login</code> endpoint. Send a
    -username and password and you will get the user's token or a new one will be
    -generated. At the moment it is not possible to register via the API.</p>
    +<h6>JWT Authentication</h6>
    +
    +<p>
    +You can generate access token via <code>/token/</code> endpoint. Send a username and password, and you will get the
    +<code>access</code> token which you can use to access the private endpoints.
    +<pre>
    +curl \
    +  -X POST \
    +  -H "Content-Type: application/json" \
    +  -d '{"username": "example_username", "password": "example_password "}' \
    +  https://wger.de/api/v2/token/
    +
    +...
    +{
    +  "access":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiY29sZF9zdHVmZiI6IuKYgyIsImV4cCI6MTIzNDU2LCJqdGkiOiJmZDJmOWQ1ZTFhN2M0MmU4OTQ5MzVlMzYyYmNhOGJjYSJ9.NHlztMGER7UADHZJlxNG0WSi22a2KaYSfd1S-AuT7lU",
    +  "refresh":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImNvbGRfc3R1ZmYiOiLimIMiLCJleHAiOjIzNDU2NywianRpIjoiZGUxMmY0ZTY3MDY4NDI3ODg5ZjE1YWMyNzcwZGEwNTEifQ.aEoAYkSJjoWH1boshQAaTkf8G3yn0kapko6HFRt7Rh4"
    +}
    +</pre>
    +
    +<p>Additionally, you can send an access token to <code>/token/verify/</code> endpoint to verify that token.</p>
    +
    +<p>When this short-lived access token expires, you can use the longer-lived <code>refresh</code>
    +token to obtain another access token.
    +<pre>
    +curl \
    +  -X POST \
    +  -H "Content-Type: application/json" \
    +  -d '{"refresh":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImNvbGRfc3R1ZmYiOiLimIMiLCJleHAiOjIzNDU2NywianRpIjoiZGUxMmY0ZTY3MDY4NDI3ODg5ZjE1YWMyNzcwZGEwNTEifQ.aEoAYkSJjoWH1boshQAaTkf8G3yn0kapko6HFRt7Rh4"}' \
    +  https://wger.de/api/v2/token/refresh/
    +
    +...
    +{"access":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiY29sZF9zdHVmZiI6IuKYgyIsImV4cCI6MTIzNTY3LCJqdGkiOiJjNzE4ZTVkNjgzZWQ0NTQyYTU0NWJkM2VmMGI0ZGQ0ZSJ9.ekxRxgb9OKmHkfy-zs1Ro_xs1eMLXiR17dIDBVxeT-w"}
    +</pre>
    +</p>
    +</p>
     
     <p>You should always use HTTPS if possible when communicating with the server.</p>
    +<p>At the moment it is not possible to register via the API.</p>
    +<p><strong>Deprecated: </strong>You can also generate a token via the <code>login</code> endpoint. Send a
    +username and password, and you will get the user's token or a new one will be
    +generated.</p>
     
     
     <div class="container">
    
  • wger/urls.py+10 2 modified
    @@ -30,6 +30,11 @@
     # Third Party
     from django_email_verification import urls as email_urls
     from rest_framework import routers
    +from rest_framework_simplejwt.views import (
    +    TokenObtainPairView,
    +    TokenRefreshView,
    +    TokenVerifyView,
    +)
     
     # wger
     from wger.core.api import views as core_api_views
    @@ -44,7 +49,7 @@
     from wger.weight.api import views as weight_api_views
     
     
    -#admin.autodiscover()
    +# admin.autodiscover()
     
     #
     # REST API
    @@ -204,7 +209,7 @@
     # The actual URLs
     #
     urlpatterns = i18n_patterns(
    -    #url(r'^admin/', admin.site.urls),
    +    # url(r'^admin/', admin.site.urls),
         path('', include(('wger.core.urls', 'core'), namespace='core')),
         path('workout/', include(('wger.manager.urls', 'manager'), namespace='manager')),
         path('exercise/', include(('wger.exercises.urls', 'exercise'), namespace='exercise')),
    @@ -244,6 +249,9 @@
             core_api_views.UserAPIRegistrationViewSet.as_view({'post': 'post'}),
             name='api_register'
         ),
    +    path('api/v2/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    +    path('api/v2/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    +    path('api/v2/token/verify/', TokenVerifyView.as_view(), name='token_verify'),
     
         # Others
         path(
    

Vulnerability mechanics

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

References

4

News mentions

0

No linked articles in our index yet.