Improper Restriction of Excessive Authentication Attempts in wger-project/wger
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.
| Package | Affected versions | Patched versions |
|---|---|---|
wgerPyPI | < 2.2 | 2.2 |
Affected products
2- wger-project/wger-project/wgerv5Range: unspecified
Patches
15e3167e3a2dcMerge branch 'master' into add-django-axes
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
4News mentions
0No linked articles in our index yet.