Skip to content
Heist/ feed
Back to feed
High

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

Type: advisorySources: OSV

### 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 **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

Products
wger
Affected versions
1.1, 1.1.1, 1.2, 1.2rc1, 1.3, 1.4, 1.5, 1.6, 1.6.1, 1.7, 1.8, 1.9, 2.0, 2.1
Fixed versions
Not specified
CVEs
CVE-2026-43976
Confirmed fixed CVEs
Not specified
Updated in this feed
30 Oct 2026, 18:13 UTC · Revision 1

Source evidence

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

core-osv · Original source

Published 15 Oct 2026, 10:22 UTC

### 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 **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

osv:PYSEC-2026-4313

wger

Fixed versions: Not specified

Complete source record
{
  "id": "d2d699ddca404db112f233b966e173fb37249cd80c8b467a8eb06136582a04ef",
  "url": "https://osv.dev/vulnerability/PYSEC-2026-4313",
  "method": "structured",
  "payload": {
    "kind": "advisory",
    "title": "wger: cross-tenant admin notes/contracts leak via gym=None bypass (5 views)",
    "summary": "### 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 **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 ",
    "evidence": "osv:PYSEC-2026-4313",
    "products": [
      {
        "name": {
          "value": "wger",
          "evidence": "/affected/0/package/name"
        },
        "prerequisites": [],
        "fixed_versions": [],
        "affected_versions": [
          {
            "value": "1.1",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "1.1.1",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "1.2",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "1.2rc1",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "1.3",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "1.4",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "1.5",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "1.6",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "1.6.1",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "1.7",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "1.8",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "1.9",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "2.0",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "2.1",
            "evidence": "/affected/0/versions"
          }
        ]
      }
    ],
    "severity": "high",
    "structured": {
      "id": "PYSEC-2026-4313",
      "aliases": [
        "CVE-2026-43976",
        "GHSA-c72h-82w6-rqfp",
        "PYSEC-2026-4313"
      ],
      "metrics": [
        {
          "type": "CVSS_V3",
          "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N"
        }
      ],
      "affected": [
        {
          "ranges": [
            {
              "type": "ECOSYSTEM",
              "events": [
                {
                  "introduced": "0"
                },
                {
                  "last_affected": "2.1"
                }
              ]
            }
          ],
          "package": {
            "name": "wger",
            "purl": "pkg:pypi/wger",
            "ecosystem": "PyPI"
          },
          "versions": [
            "1.1",
            "1.1.1",
            "1.2",
            "1.2rc1",
            "1.3",
            "1.4",
            "1.5",
            "1.6",
            "1.6.1",
            "1.7",
            "1.8",
            "1.9",
            "2.0",
            "2.1"
          ],
          "database_specific": {
            "source": "https://github.com/pypa/advisory-database/blob/main/vulns/wger/PYSEC-2026-4313.yaml"
          }
        }
      ],
      "modified": "2026-10-15T14:45:10.349862114Z",
      "provider": "osv",
      "references": [
        "https://github.com/wger-project/wger/security/advisories/GHSA-c72h-82w6-rqfp",
        "https://github.com/wger-project/wger",
        "https://github.com/wger-project/wger/releases/tag/2.6",
        "https://pypi.org/project/wger",
        "https://github.com/advisories/GHSA-c72h-82w6-rqfp",
        "https://nvd.nist.gov/vuln/detail/CVE-2026-43976"
      ]
    },
    "related_cves": [
      {
        "value": "CVE-2026-43976",
        "evidence": "/aliases"
      }
    ],
    "security_relevant": true,
    "severity_evidence": "/severity/0/score"
  },
  "source_id": "core-osv",
  "document_id": 332651,
  "published_at": "2026-10-15T10:22:46.832742+00:00"
}