KEVWire

Security intelligence, with context.

Response snapshot Oct 27, 2026, 00:34 UTC

Back to the wire
Permalink
VulnerabilityCVE-2026-43976

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

Five gym management views in wger apply a flawed gym-scope guard (`gym_a != gym_b`) that silently passes when both operands are `None`. A trainer with `gym.gym_trainer` and `gym.

At a glance

Exploited
Not known
Severity
7.1 High (CVSS 3.1)
Chance of exploitation
0.2% in the next 30 days
Sources
1 source

What to do

Consult the exact GitHub advisory for affected versions, mitigation, and patch availability.

Read GitHub-reviewed advisory GHSA-c72h-82w6-rqfp (opens a new tab)

What happened

### Summary

Five gym management views in wger apply a flawed gym-scope guard (`gym_a != gym_b`) that silently passes when both operands are `None`. A trainer with `gym.gym_trainer` and `gym.add_adminusernote` 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 `member_id` with no secondary gym-scoped validation, so all records are disclosed.

### Details

Continue reading

**Files**: `wger/gym/views/user.py`, `wger/gym/views/admin_notes.py`, `wger/gym/views/document.py`, `wger/gym/views/contract.py`, `wger/gym/views/user_config.py`

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

```python # VULNERABLE - applied in admin_notes_list, documents_list, contracts_list, # user_config, and gym_permissions_user_edit 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 member_id ```

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/ ` -> admin notes list view - `GET /en/gym/documents/list/user/ ` -> documents list view - `GET /en/gym/contract/list/ ` -> contracts list view - `GET /en/gym/user/ /config` -> user config view - `GET /en/gym/user/ /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() + trainer_gym_id = request.user.userprofile.gym_id + member_gym_id = user.userprofile.gym_id + + if trainer_gym_id is None or trainer_gym_id != member_gym_id: + return HttpResponseForbidden()

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

Extract a shared helper `assert_same_gym(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.gym_trainer` + `gym.add_adminusernote` 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=[trainer1_session] ```

**Step 2** - Read victim's private admin notes:

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

-> 200 OK body contains all private admin notes for user 2: "PRIVATE_NOTE_ABOUT_ALICE_SALARY_50K" "PHASE4_SECRET_SALARY_100K" ```

**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.gym_trainer` + `gym.add_adminusernote` 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.gym_trainer` + `gym.add_adminusernote` 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 `trainer_login` and `reset_user_password` (submitted separately). The root cause - Django ORM object-`!=` returning `False` when both sides are `None` - warrants a shared `same_gym()` helper applied across all five views.

Affected package ranges reported by GitHub: wger: <= 2.1

Scores and how they were measured

CVSS base severity

7.1

Version 3.1As of

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N

Measurement source (opens a new tab)

How bad it is if exploited. Not a prediction of exploitation or proof you are exposed.

EPSS probability

0.2%

Scored Percentile 12.3%

Model and measurement source (opens a new tab)

A model's estimate of exploitation in the next 30 days, not certainty or your own risk.

Related technology

Sources

  1. Primary source

    GitHub-reviewed advisory GHSA-c72h-82w6-rqfp (opens a new tab)

    The public advisory supplies the description, affected package/version ranges, and any listed CVSS metric. GitHub review is not local review.

    Published Checked

  2. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-c72h-82w6-rqfp, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

  3. Primary source

    FIRST measured EPSS score (opens a new tab)

    Measured EPSS probability and percentile for CVE-2026-43976, scored 2026-10-26. This is a model estimate, not proof of exposure.

    Published Checked

Cite this record

For factual claims, cite the primary sources above; cite KEVWire for its compilation, dates, and classifications.

APA

KEVWire. (2026, October 26). wger: cross-tenant admin notes/contracts leak via gym=None bypass (5 views) (CVE-2026-43976). https://kevwire.com/record/CVE-2026-43976

MLA

"wger: cross-tenant admin notes/contracts leak via gym=None bypass (5 views) (CVE-2026-43976)." KEVWire, 26 Oct. 2026, kevwire.com/record/CVE-2026-43976.

Chicago

KEVWire. "wger: cross-tenant admin notes/contracts leak via gym=None bypass (5 views) (CVE-2026-43976)." Last modified October 26, 2026. https://kevwire.com/record/CVE-2026-43976.

Evidence summary (plain text)

wger: cross-tenant admin notes/contracts leak via gym=None bypass (5 views) (Oct 7, 2026).
Five gym management views in wger apply a flawed gym-scope guard (`gym_a != gym_b`) that silently passes when both operands are `None`. A trainer with `gym.gym_trainer` and `gym.add_adminusernote` 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 `member_id` with no secondary gym-scoped validation, so all records are disclosed.

**Files**: `wger/gym/views/user.py`, `wger/gym/views/admin_notes.py`, `wger/gym/views/document.py`, `wger/gym/views/contract.py`, `wger/gym/views/user_config.py`
CVSS 7.1 (version 3.1; evidence as of Oct 26, 2026; https://github.com/advisories/GHSA-c72h-82w6-rqfp).
EPSS 0.2% for the following 30 days, scored Oct 26, 2026 (https://api.first.org/data/v1/epss?cve=CVE-2026-43976&date=2026-10-26).
Sources:
GitHub-reviewed advisory GHSA-c72h-82w6-rqfp: https://github.com/advisories/GHSA-c72h-82w6-rqfp
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc
FIRST measured EPSS score: https://api.first.org/data/v1/epss?cve=CVE-2026-43976&date=2026-10-26
Record ID: CVE-2026-43976.
Record origin: Ingested record.
AI evidence: Not assessed.
Known exploitation: Not assessed.
Zero-day status: Not assessed. KEV membership alone does not establish zero-day status.
About this data

This record was ingested from a data source. Retrieved: Oct 26, 2026, 19:40 UTC. Ingestion is not an independent editorial investigation.

A CISA KEV listing documents known exploitation, not necessarily a zero-day; absence from the catalog does not prove a vulnerability has never been exploited.

Technology tags indicate possible relevance, not verified exposure. Confirm the affected product, version, and configuration against your own inventory; KEVWire does not scan your systems.

A personal reading checklist on this device, not verified remediation.

Record JSON

Your account

Email alerts

Email alerts are not available yet. To follow your stack today, use its RSS feed, which honors your filters, or connect an AI agent through the API.

Suggest an improvement

Ideas, missing sources, corrections, and bugs are all welcome. No account needed. Suggestions are reviewed by a person before anything changes.

Record details

Record detail