Python
CVE-2026-40474
HIGH
Attack ChainAIDerived
Hypothetical attack flow derived from CVE metadata – not a confirmed exploit chain
-
AccessAuthenticate as low-privilege user
-
DeliveryNavigate to /config/gym-config/edit
-
ExploitSubmit POST with modified default_gym value
-
ExecutionTrigger GymConfig.save() side effects
-
PersistInstallation-wide user profile reassignment completes
-
ImpactAccess tenant-isolated user data in target gym
Vulnerability Assessment
| Weakness |
CWE
OWASP Top 10:2025
A01:2025 - Broken Access Control
ASVS 5.0
V8 - Authorization Access control enforcement
MITRE ATT&CK
1 technique across 1 tactic: Defense Evasion The technique list with ATT&CK links and the kill-chain view is part of the Basic plan and up. |
| Exploitation Conditions | Authenticated access with any valid user account (PR:L in CVSS vector). No administrative privileges, specific roles, or group memberships required-any registered user can exploit this. The vulnerability affects default configurations where GymConfigUpdateView is accessible via standard URL routing at /config/gym-config/edit. Exploitation requires the wger installation to have the config.gym_config module installed and URL patterns mapped (confirmed in default deployments). The target installation must use GymConfig singleton (pk=1) with the vulnerable mixin inheritance-this is the upstream implementation as shown in wger/config/views/gym_config.py. Attack is network-based (AV:N), does not require user interaction (UI:N), and succeeds against standard Docker Compose deployments as demonstrated in the POC. Limiting factor: attacker must know or discover the /config/gym-config/edit endpoint URL, though this is trivial via source code review or directory enumeration on open-source deployments. |
| Impact | Real-world risk is HIGH for production wger installations with multiple gyms/tenants, MODERATE for single-gym deployments. CVSS 7.6 accurately reflects the technical severity: network-accessible (AV:N), trivial to exploit once authenticated (AC:L/PR:L), with significant integrity impact (I:H) from unauthorized tenant manipulation. EPSS data not available. No CISA KEV listing indicates no confirmed widespread exploitation. The GitHub security advisory provides sufficient technical detail for proof-of-concept development, lowering the bar for exploitation. SSVC assessment: exploitation status is none (advisory-only), automatable is yes (simple authenticated POST request), technical impact is partial (manipulates global state but doesn't grant direct code execution). The vulnerability's severity is amplified by GymConfig.save() side effects that perform bulk database updates affecting other users-this is not merely unauthorized access to a settings page but actual privilege escalation enabling installation-wide data manipulation. Single-tenant deployments see reduced impact as gym reassignment affects fewer boundaries. Defenders should prioritize patching if running multi-gym/SaaS configurations; single-gym users can deprioritize if the default_gym setting is not operationally relevant. |
| Exploit ScenarioDerived | An attacker registers a standard user account on a multi-tenant wger installation hosting multiple gyms. They navigate directly to /config/gym-config/edit, bypassing any UI restrictions, and submit a POST request changing the default_gym foreign key to a gym they wish to monitor or disrupt. The GymConfig.save() method executes, reassigning all UserProfile records with gym=None to the attacker-selected gym and creating GymUserConfig entries that may grant the attacker visibility into another tenant's user roster or training data. If the attacker controls or can register into the target gym, they gain access to user records that should be isolated by tenant boundaries. |
Get an e-mail when this CVE is exploited, patched or re-scored. Free account.
Alert meAnalysisAIv1 – Apr 16, 2026
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.
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.
Affected Packages
Source: OSV (Open Source Vulnerabilities)
Affects your stack
Affects 1 package in pypi
Checking this CVE against your own stacks and servers is part of the Basic plan and up.
Technical ContextAI · show details
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.
Know when Python gets its next vulnerability
- Watch products and get an email when something new is published
- Check this CVE against your own stack and servers
- Exploit and PoC references, threat intelligence from 22 sources
No credit card · 7-day full trial
Severity by source
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
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
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
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
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
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
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() methodBecause 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
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:
default_gym_id= None
profiles_gym_null= 1After a low-privileged user submitted the form setting default_gym to gym id 1:
default_gym_id= 1
profiles_gym_null= 0Recommended fix
Ensure permission enforcement runs before the form dispatch.
Using the project mixin (order matters):
class GymConfigUpdateView(WgerPermissionMixin, WgerFormMixin, UpdateView):
permission_required = 'config.change_gymconfig'
login_required = TrueAlternatively, 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" />
Executive ViewAI
CVE-2026-40474 is a privilege escalation vulnerability in wger that allows authenticated low-privileged users to modify organization-wide gym configuration settings due to missing permission enforcement in the web interface. This could enable unauthorized users to manipulate default gym assignments affecting all users across the installation, potentially disrupting operational gym management and user experience. Organizations running wger should assess exposure and implement mitigations immediately.
No vendor-released patch identified at time of analysis. Implement network-level access controls restricting /config/gym-config/edit endpoint to administrative IP ranges or VPN; apply authentication via reverse proxy with additional Multi-Factor Authentication requirement; conduct immediate code review of GymConfigUpdateView to manually enforce permission checks at application level pending official patch; restrict gym configuration modification to administrative roles only via database-level constraints until patched version is available.
Threat Intelligence Overview
How this decision was reached
The engine reads each signal below and applies a fixed list of rules from top to bottom. The first rule that matches sets the recommended action – like a statement, every line is shown.
What every action level means
SSVC AssessmentENISA
Priority Score
Attack Technique
Authentication bypass attacks exploit flaws in the verification mechanisms that control access to systems and applications.
Privilege escalation occurs when an attacker leverages flaws in access control mechanisms to gain permissions beyond what they were originally granted.
Affected Products (CPE)GitHub Advisory
Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t
BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser
pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi
The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python
BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica
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
pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.
Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301
In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse
Unauthenticated remote code execution affects Kestra OSS (the open-source event-driven orchestration platform) prior to
Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/
pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne
GHSA-xppv-4jrx-qf8m
NVD
CVE.org
EPSS
Vulnrichment
Exploit-DB