Skip to main content

Python CVE-2026-40474

HIGH
Improper Access Control (CWE-284)
2026-04-16 https://github.com/wger-project/wger GHSA-xppv-4jrx-qf8m
7.6
CVSS 3.1 · GitHub Advisory
Share

Severity by source

GitHub Advisory PRIMARY
7.6 HIGH
AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:L

Primary rating from GitHub Advisory · only source for this CVE.

CVSS VectorGitHub Advisory

Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
Confidentiality
Low
Integrity
High
Availability
Low

Lifecycle Timeline

4
Re-analysis Queued
Apr 17, 2026 - 22:22 vuln.today
cvss_changed
Analysis Generated
Apr 16, 2026 - 01:50 vuln.today
Analysis Generated
Apr 16, 2026 - 01:45 vuln.today
CVE Published
Apr 16, 2026 - 01:35 nvd
HIGH 7.6

DescriptionGitHub Advisory

Summary

wger exposes a global configuration edit endpoint at /config/gym-config/edit implemented by GymConfigUpdateView. The view declares permission_required = 'config.change_gymconfig' but does not enforce it because it inherits WgerFormMixin (ownership-only checks) instead of the project’s permission-enforcing mixin (WgerPermissionMixin) .

The edited object is a singleton (GymConfig(pk=1)) and the model does not implement get_owner_object(), so WgerFormMixin skips ownership enforcement. As a result, a low-privileged authenticated user can modify installation-wide configuration and trigger server-side side effects in GymConfig.save().

This is a vertical privilege escalation from a regular user to privileged global configuration control. The application explicitly declares permission_required = 'config.change_gymconfig', demonstrating that the action is intended to be restricted; however, this requirement is never enforced at runtime.

Affected endpoint

The config URLs map as follows.

File: wger/config/urls.py

python
patterns_gym_config = [
    path('edit', gym_config.GymConfigUpdateView.as_view(), name='edit'),
]

urlpatterns = [
    path(
        'gym-config/',
        include((patterns_gym_config, 'gym_config'), namespace='gym_config'),
    ),
]

This resolves to:

/config/gym-config/edit

Root cause

The view declares a permission but does not enforce it

File: wger/config/views/gym_config.py

python
class GymConfigUpdateView(WgerFormMixin, UpdateView):
    model = GymConfig
    fields = ('default_gym',)
    permission_required = 'config.change_gymconfig'
    success_url = reverse_lazy('gym:gym:list')
    title = gettext_lazy('Edit')

    def get_object(self):
        return GymConfig.objects.get(pk=1)

The permission string exists, but WgerFormMixin does not check permission_required.

The project’s permission mixin exists but is not used

File: wger/utils/generic_views.py

python
class WgerPermissionMixin:
    permission_required = False
    login_required = False

    def dispatch(self, request, *args, **kwargs):
        if self.login_required or self.permission_required:
            if not request.user.is_authenticated:
                return HttpResponseRedirect(
                    reverse_lazy('core:user:login') + f'?next={request.path}'
                )

            if self.permission_required:
                has_permission = False
                if isinstance(self.permission_required, tuple):
                    for permission in self.permission_required:
                        if request.user.has_perm(permission):
                            has_permission = True
                elif request.user.has_perm(self.permission_required):
                    has_permission = True

                if not has_permission:
                    return HttpResponseForbidden('You are not allowed to access this object')

        return super(WgerPermissionMixin, self).dispatch(request, *args, **kwargs)

GymConfigUpdateView does not inherit this mixin, so none of the login/permission logic runs.

The mixin that *is* used performs only ownership checks, and GymConfig has no owner

File: wger/utils/generic_views.py

python
class WgerFormMixin(ModelFormMixin):
    def dispatch(self, request, *args, **kwargs):
        self.kwargs = kwargs
        self.request = request

        if self.owner_object:
            owner_object = self.owner_object['class'].objects.get(pk=kwargs[self.owner_object['pk']])
        else:
            try:
                owner_object = self.get_object().get_owner_object()
            except AttributeError:
                owner_object = False

        if owner_object and owner_object.user != self.request.user:
            return HttpResponseForbidden('You are not allowed to access this object')

        return super(WgerFormMixin, self).dispatch(request, *args, **kwargs)

File: wger/config/models/gym_config.py

python
class GymConfig(models.Model):
    default_gym = models.ForeignKey(
        Gym,
        verbose_name=_('Default gym'),
# ...
        null=True,
        blank=True,
        on_delete=models.CASCADE,
    )
# No get_owner_object() method

Because GymConfig does not implement get_owner_object(), WgerFormMixin catches AttributeError and sets owner_object = False, skipping any access restriction.

Security impact

This is not a cosmetic setting: GymConfig.save() performs installation-wide side effects.

File: wger/config/models/gym_config.py

python
def save(self, *args, **kwargs):
    if self.default_gym:
        UserProfile.objects.filter(gym=None).update(gym=self.default_gym)

        for profile in UserProfile.objects.filter(gym=self.default_gym):
            user = profile.user
            if not is_any_gym_admin(user):
                try:
                    user.gymuserconfig
                except GymUserConfig.DoesNotExist:
                    config = GymUserConfig()
                    config.gym = self.default_gym
                    config.user = user
                    config.save()

    return super(GymConfig, self).save(*args, **kwargs)

On deployments with multiple gyms, this allows a low-privileged user to tamper with tenant assignment defaults, affecting new registrations and bulk-updating existing users lacking a gym. This permits unauthorized modification of installation-wide state and bulk updates to other users’ records, violating the intended administrative trust boundary.

Proof of concept (local verification)

Environment: local docker compose stack, accessed via http://127.0.0.1:8088/en/.

Observed behavior

An unauthenticated user can reach the endpoint via GET; POST requires authentication and redirects to login. An authenticated low-privileged user can submit the form and change the global singleton. After the save, the application redirects to success_url = reverse_lazy('gym:gym:list') (e.g. /en/gym/list), which is permission-protected; therefore the browser may display a “Forbidden” page even though the global update already succeeded.

DB evidence (before/after)

Before submission:

bash
default_gym_id= None
profiles_gym_null= 1

After a low-privileged user submitted the form setting default_gym to gym id 1:

bash
default_gym_id= 1
profiles_gym_null= 0

Recommended fix

Ensure permission enforcement runs before the form dispatch.

Using the project mixin (order matters):

python
class GymConfigUpdateView(WgerPermissionMixin, WgerFormMixin, UpdateView):
    permission_required = 'config.change_gymconfig'
    login_required = True

Alternatively, use Django’s PermissionRequiredMixin (and LoginRequiredMixin) directly.

Conclusion

The view explicitly declares permission_required = 'config.change_gymconfig', which demonstrates developer intent that this action be restricted. The fact that it is not enforced constitutes improper access control regardless of perceived business impact.

<img width="1912" height="578" alt="Screenshot 2026-02-27 230752" src="https://github.com/user-attachments/assets/c627b404-6d9c-4477-88bd-f867d0fa09d2" />

AnalysisAI

Authenticated low-privileged users in wger can modify installation-wide gym configuration via /config/gym-config/edit due to missing permission enforcement, enabling vertical privilege escalation. The GymConfigUpdateView declares 'config.change_gymconfig' permission but inherits WgerFormMixin instead of WgerPermissionMixin, causing the permission check to never execute. Exploiting this allows attackers to manipulate default gym assignments affecting all users, with GymConfig.save() automatically reassigning user profiles and creating gym configurations tenant-wide. CVSS 7.6 (High) with network attack vector, low complexity, and low privileges required. No active exploitation (KEV) or public POC identified at time of analysis, though GitHub advisory provides detailed reproduction steps.

Technical ContextAI

The vulnerability stems from Django class-based view mixin inheritance order and method resolution. The wger project implements two mixins: WgerPermissionMixin (checks user.has_perm() against permission_required in dispatch()) and WgerFormMixin (validates object ownership via get_owner_object()). GymConfigUpdateView inherits only WgerFormMixin, which attempts ownership validation but fails gracefully when GymConfig model lacks get_owner_object() method, returning owner_object=False and skipping all access control. The permission_required attribute exists as a class variable but is never evaluated because WgerPermissionMixin.dispatch() is not in the method resolution order. This is a textbook CWE-284 (Improper Access Control) where authorization logic is defined but not executed. The GymConfig singleton (pk=1) represents installation-wide settings, and its save() method performs CASCADE operations on UserProfile and GymUserConfig tables, making this a high-impact data integrity issue in multi-tenant deployments.

RemediationAI

Apply the vendor-recommended fix by modifying GymConfigUpdateView inheritance to include WgerPermissionMixin before WgerFormMixin in the class definition: 'class GymConfigUpdateView(WgerPermissionMixin, WgerFormMixin, UpdateView)' with 'login_required = True' set. Python's method resolution order (MRO) processes mixins left-to-right, so WgerPermissionMixin.dispatch() will execute before WgerFormMixin.dispatch(), enforcing the permission_required check. Alternatively, replace custom mixins with Django's built-in PermissionRequiredMixin and LoginRequiredMixin for standards-compliant access control. Patch availability and fixed version number not confirmed in available data-monitor https://github.com/wger-project/wger/security/advisories/GHSA-xppv-4jrx-qf8m for release information. Compensating control if immediate patching is not feasible: implement reverse proxy or Django middleware to restrict /config/gym-config/edit to superuser role via HTTP 403 response; this prevents unauthorized access but does not fix the underlying authorization flaw and will conflict with legitimate administrator workflows. Workaround side effects: middleware-based blocking requires maintaining URL pattern allowlists and may break if application URL routing changes. Verify fix effectiveness by testing that low-privileged authenticated users receive HTTP 403 when accessing /config/gym-config/edit.

More in Python

View all
CVE-2025-24016 CRITICAL POC
9.9 Feb 10

Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t

CVE-2025-27520 CRITICAL POC
9.8 Apr 04

BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser

CVE-2025-2945 CRITICAL POC
9.9 Apr 03

pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi

CVE-2013-5093 MEDIUM POC
6.8 Sep 27

The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python

CVE-2025-32375 CRITICAL POC
9.8 Apr 09

BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica

CVE-2014-0224 HIGH POC
7.4 Jun 05

OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph

CVE-2024-21644 HIGH POC
7.5 Jan 08

pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.

CVE-2026-33017 CRITICAL POC
9.3 Mar 17

Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301

CVE-2017-9462 HIGH POC
8.8 Jun 06

In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse

CVE-2026-49869 CRITICAL POC
10.0 Jun 26

Unauthenticated remote code execution affects Kestra OSS (the open-source event-driven orchestration platform) prior to

CVE-2026-39987 CRITICAL POC
9.3 Apr 08

Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/

CVE-2024-21645 MEDIUM POC
5.3 Jan 08

pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne

Share

CVE-2026-40474 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy