Skip to content

Latest commit

 

History

History
35 lines (22 loc) · 3.42 KB

File metadata and controls

35 lines (22 loc) · 3.42 KB

wger — Security Research

wger is a self-hosted Django-based fitness/workout-tracking platform with a "gym mode" that lets trainers manage their gym's members. The findings below were disclosed through GitHub Security Advisories.

The fix has landed on main (commit e1d329f). The latest tagged release at the time of writing is 2.5; the 2.6 release that will ship the fix has not yet been cut. If you self-host wger and run 2.5 or earlier, pin to main until 2.6 is released.

Findings

CVE Severity Class Auth Required One-line
CVE-2026-43948 Critical 9.9 Incorrect Authorization (CWE-863) Trainer with gym.manage_gym and gym=None Cross-tenant password reset + plaintext disclosure → one-shot account takeover of any unaffiliated user

Note: A sibling advisory tracking the same gym=None comparison bug across three additional information-disclosure views (admin_notes_list, documents_list, contracts_list) is currently in GHSA Draft state and will be added to this index once published. The patch on main already closes those callsites via the same is_same_gym() helper.

Common context

  • Affected: wger ≤ 2.5.0
  • Patch on main: e1d329f (2026-04-26) — "Implement better gym membership checks"
  • Tagged fix release: 2.6 (planned; not yet released as of 2026-05-05)
  • Disclosure window: GHSA-mhc8 published 2026-04-28; sibling cluster pending.

Themes

  • None != None is False. Any Django ForeignKey accessor returns None when the FK is null, and None-vs-None inequality silently passes. When a comparison is security-critical — particularly when one of the values is expected to sometimes be null — compare the underlying integer ID with an explicit is not None guard. The patch's is_same_gym(a, b) helper is the right shape: gym_a is not None and gym_a == gym_b.
  • Default user state is the attack surface. "Trainer with gym=None" sounds like an unusual configuration until you realise it is the default state of every newly created trainer account before manual gym linking. Bugs keyed off "the default" are the most realistic to exploit.
  • Plaintext credentials in response bodies multiply impact. Even after the authorization bypass is fixed, the underlying response template that returns a freshly generated password verbatim in HTML is a hardening target on its own. Pair every "reset password" flow with an out-of-band delivery (email link, one-time setup token) so the attacker can't read the credential out of the response.
  • Variant analysis on shape, not name. The same comparison pattern shows up in five views in this codebase — a grep for userprofile.gym != would have surfaced all of them at once. When triaging an authorization bug, look at the structure of the check (A.fk != B.fk) and search the codebase for that pattern, not just the named function.

Reporter

Found and reported by @whatisproblem.

Acknowledgement

Thanks to the wger maintainers (@rolandgeider and the wger-project organisation) for the prompt fix turnaround and for centralising the new check into a reusable is_same_gym() helper.