CVE-2026-43976: wger: cross-tenant admin notes/contracts leak via gym=None bypass (5 views)

Published Oct 7, 2026
·
Updated

Summary

Five gym management views in wger apply a flawed gym-scope guard (gyma != gymb) that silently passes when both operands are None. A trainer with gym.gymtrainer and gym.addadminusernote permissions and no gym assignment (gym=None) can read private admin notes, uploaded documents, gym contracts, user configuration, and user permission data for any other unaffiliated user on the instance. The subsequent querysets filter only on the attacker-supplied memberid with no secondary gym-scoped validation, so all records are disclosed.

Details

Files: wger/gym/views/user.py, wger/gym/views/adminnotes.py, wger/gym/views/document.py, wger/gym/views/contract.py, wger/gym/views/userconfig.py

The same flawed comparison pattern appears across at least five views:

python VULNERABLE - applied in adminnoteslist, documentslist, contractslist, userconfig, and gympermissionsuseredit if request.user.userprofile.gym != user.userprofile.gym: return HttpResponseForbidden()

After the guard (admin notes example): notes = AdminUserNote.objects.filter(member=member) # only filtered by memberid

When both request.user.userprofile.gym and user.userprofile.gym are None, Python evaluates None != None as False, and HttpResponseForbidden is never reached. The subsequent queryset applies only the attacker-supplied member (user ID) as a filter — there is no secondary check tying the queryset to the requesting trainer's gym. All private admin notes, documents, and contracts for the target user are returned in the response body.

Affected endpoints: - GET /en/gym/notes/list/user/<memberpk> -> admin notes list view - GET /en/gym/documents/list/user/<memberpk> -> documents list view - GET /en/gym/contract/list/<memberpk> -> contracts list view - GET /en/gym/user/<memberpk>/config -> user config view - GET /en/gym/user/<memberpk>/permissions -> permission edit view

Suggested patch:

diff --- a/wger/gym/views/user.py +++ b/wger/gym/views/user.py - if request.user.userprofile.gym != user.userprofile.gym: - return HttpResponseForbidden() + trainergymid = request.user.userprofile.gymid + membergymid = user.userprofile.gymid + + if trainergymid is None or trainergymid != membergymid: + return HttpResponseForbidden()

Also tighten the queryset with a gym-scoped secondary filter: - notes = AdminUserNote.objects.filter(member=member) + notes = AdminUserNote.objects.filter( + member=member, + memberuserprofilegymid=request.user.userprofile.gymid, + )

Extract a shared helper assertsamegym(trainer, member) and call it consistently from all five affected views to eliminate future drift.

PoC

Tested on wger/server:latest Docker image. Test users: trainer1 (gym.gymtrainer + gym.addadminusernote permissions, userprofile.gym=None) and alice (regular user, userprofile.gym=None, has a private admin note pre-seeded).

Step 1 - Authenticate as trainer with required perms and gym=None:

POST /en/user/login HTTP/1.1 Host: target Content-Type: application/x-www-form-urlencoded

username=trainer1&password=[REDACTED]&csrfmiddlewaretoken=[REDACTED]

-> 302 Found; Set-Cookie: sessionid=[trainer1session]

Step 2 - Read victim's private admin notes:

GET /en/gym/notes/list/user/2 HTTP/1.1 Host: target Cookie: sessionid=[trainer1session]

-> 200 OK body contains all private admin notes for user 2: "PRIVATENOTEABOUTALICESALARY50K" "PHASE4SECRETSALARY100K"

Step 3 - Read victim's documents and contracts (same pattern):

GET /en/gym/documents/list/user/2 GET /en/gym/contract/list/2

-> 200 OK for each; all records disclosed

Step 4 (optional) - Mass enumeration across all gym=None users:

Iterate user PKs 1..N: GET /en/gym/notes/list/user/{uid} -> 200 = gym=None victim (notes leaked) -> 403 = gym-assigned user (check works correctly when gym values differ)

RBAC Disproof Protocol: - Scenario A (admin, gym=1 -> member gym=1) -> HTTP 200 (expected - same-gym read is a documented feature) - Scenario B (trainer1, gym=None -> alice gym=None) -> HTTP 200 with PII in body (expected HTTP 403) - Scenario C (admin, gym=1 -> alice gym=None) -> HTTP 403 (expected - guard works when gyms differ; confirms bypass is None-specific)

Reproducibility: 2/2 runs after clean-baseline database reset.

Impact

An authenticated trainer with gym.gymtrainer + gym.addadminusernote permissions and userprofile.gym=None can:

1. Enumerate and read all private admin notes for every other gym=None user (notes may contain salary data, medical notes, disciplinary records). 2. Download all uploaded documents attached to those users (contracts, ID scans, medical forms). 3. Read all gym contracts (financial terms, subscription details). 4. Read user configuration details. 5. Via the permissions endpoint, potentially modify victim permissions (creates a privilege escalation path - not fully explored in this submission but the endpoint is reachable).

Affected deployments: every wger instance where gym.gymtrainer + gym.addadminusernote are delegated to non-admin users AND any other users exist with gym=None. The gym=None state is the default for newly registered users before manual gym assignment, so every public-registration wger instance is affected.

Severity: High (CVSS 7.1). Network-reachable, low complexity, requires only low privilege (delegated trainer), scope unchanged (same wger authority), high confidentiality loss across all unaffiliated accounts, low integrity impact (permission-edit view reachable).

This is structurally the same bug class as the sibling findings affecting trainerlogin and resetuserpassword (submitted separately). The root cause - Django ORM object-!= returning False when both sides are None - warrants a shared samegym() helper applied across all five views.

Other sources

wger is a free, open-source workout and fitness manager. Prior to version 2.6, five gym management views in wger apply a flawed gym-scope guard (gyma != gymb) that silently passes when both operands are None. A trainer with gym.gymtrainer and gym.addadminusernote permissions and no gym assignment (gym=None) can read private admin notes, uploaded documents, gym contracts, user configuration, and user permission data for any other unaffiliated user on the instance. The subsequent querysets filter only on the attacker-supplied memberid with no secondary gym-scoped validation, so all records are disclosed. Version 2.6 fixes the issue.

— MITRE

Affected Software

1 affected component
pip/wger<=2.1

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade wger to a version that resolves this vulnerability.

    Fixed in 2.6

Event History

Oct 7, 2026
CVE Published
via MITRE·01:33 PM
Data Sourced
via MITRE·01:33 PM
DescriptionSeverityWeakness
Advisory Published
via GitHub·01:33 PM
Data Sourced
via GitHub·01:33 PM
DescriptionSeverityWeaknessAffected Software
Data Sourced
via NVD·02:17 PM
DescriptionSeverityWeakness

Frequently Asked Questions

1

Who can exploit this issue?

An authenticated trainer account is required. The account must have the gym.gym_trainer and gym.add_adminusernote permissions and have no gym assignment (gym=None).

2

What data can be accessed through the affected views?

The attacker can access private admin notes, uploaded documents, gym contracts, user configuration, and user permission data for unaffiliated users on the same instance. The affected querysets are filtered by the attacker-supplied member_id without a secondary gym-scope check.

3

Does exploitation require user interaction or a complex attack?

No user interaction is required, and the listed vector indicates network access with low attack complexity. Exploitation depends on the attacker already having the required authenticated trainer permissions and an unassigned gym profile.

4

How can administrators identify potentially exposed accounts?

Review trainer accounts for profiles with gym set to None, then determine whether they hold both gym.gym_trainer and gym.add_adminusernote permissions. Those accounts meet the described authorization conditions for accessing other users' records through the affected views.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203