Skip to main content

wger CVE-2026-43948

CRITICAL
Incorrect Authorization (CWE-863)
2026-05-06 https://github.com/wger-project/wger GHSA-mhc8-p3jx-84mm
9.9
CVSS 3.1 · GitHub Advisory
Share

Severity by source

GitHub Advisory PRIMARY
9.9 CRITICAL
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
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Changed
Confidentiality
High
Integrity
High
Availability
High

Lifecycle Timeline

3
Source Code Evidence Fetched
May 06, 2026 - 20:31 vuln.today
Analysis Generated
May 06, 2026 - 20:31 vuln.today
CVE Published
May 06, 2026 - 19:50 nvd
CRITICAL 9.9

DescriptionGitHub 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:

python
# 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:

python
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_password
  • GET /en/gym/user/<user_pk>/edit -> wger.gym.views.user.gym_permissions_user_edit

Suggested patch:

diff
--- 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:

  1. Reset the password of any other gym=None user on the wger instance.
  2. Receive the new plaintext password in the HTTP response body.
  3. Log in as the victim immediately.
  4. 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.

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-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-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

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-2026-55255 HIGH POC
8.4 Jun 19

Cross-user flow execution in Langflow (< 1.9.1) lets any authenticated API-key holder run another user's flow by passing

Share

CVE-2026-43948 vulnerability details – vuln.today

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