wger: cross-tenant password reset and plaintext disclosure via gym=None bypass
Date: 2026-05
Affected: pip/wger
Attack vector: auth-bypass
Category: vulnerability-disclosure
Severity: Critical
CVE: CVE-2026-43948
CWE: CWE-863
CVSS: 9.9 (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H)

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


Mitigations:

Architectural layers (MAESTRO):