Skip to content
Heist/ feed
Back to feed
High

wger: Trainer Privilege Escalation - Improper Privilege Management

Type: advisorySources: OSV

### Summary A user with only the `gym_trainer` permission can deactivate any account in the same gym, including `gym_manager` and `general_gym_manager` accounts. The `UserDeactivateView` grants access to anyone holding **any one** of `gym.manage_gym`, `gym.manage_gyms`, or `gym.gym_trainer` (OR logic via `WgerMultiplePermissionRequiredMixin`), and performs no privilege-hierarchy check to prevent a lower-privileged role from disabling a higher-privileged one. ### Details `UserDeactivateView` (file: `wger/core/views/user.py`, line 378) is configured with: ```python permission_required = ('gym.manage_gym', 'gym.manage_gyms', 'gym.gym_trainer') ``` `WgerMultiplePermissionRequiredMixin` (file: `wger/utils/generic_views.py`, line 48) treats this tuple as an OR check -- any single permission is sufficient: ```python class WgerMultiplePermissionRequiredMixin(PermissionRequiredMixin): def has_permission(self): for permission in self.get_permission_required(): if self.request.user.has_perm(permission): return True # <-- ANY one permission is enough return False ``` The `dispatch()` method only verifies same-gym membership: ```python def dispatch(self, request, *args, **kwargs): edit_user = get_object_or_404(User, pk=self.kwargs['pk']) if ( request.user.has_perm('gym.manage_gym') or request.user.has_perm('gym.gym_trainer') ) and edit_user.userprofile.gym_id != request.user.userprofile.gym_id: return HttpResponseForbidden() # NO check: is the target user more privileged than the requeste

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-46434
Confirmed fixed CVEs
Not specified
Updated in this feed
30 Oct 2026, 18:13 UTC · Revision 1

Source evidence

wger: Trainer Privilege Escalation - Improper Privilege Management

core-osv · Original source

Published 15 Oct 2026, 10:22 UTC

### Summary A user with only the `gym_trainer` permission can deactivate any account in the same gym, including `gym_manager` and `general_gym_manager` accounts. The `UserDeactivateView` grants access to anyone holding **any one** of `gym.manage_gym`, `gym.manage_gyms`, or `gym.gym_trainer` (OR logic via `WgerMultiplePermissionRequiredMixin`), and performs no privilege-hierarchy check to prevent a lower-privileged role from disabling a higher-privileged one. ### Details `UserDeactivateView` (file: `wger/core/views/user.py`, line 378) is configured with: ```python permission_required = ('gym.manage_gym', 'gym.manage_gyms', 'gym.gym_trainer') ``` `WgerMultiplePermissionRequiredMixin` (file: `wger/utils/generic_views.py`, line 48) treats this tuple as an OR check -- any single permission is sufficient: ```python class WgerMultiplePermissionRequiredMixin(PermissionRequiredMixin): def has_permission(self): for permission in self.get_permission_required(): if self.request.user.has_perm(permission): return True # <-- ANY one permission is enough return False ``` The `dispatch()` method only verifies same-gym membership: ```python def dispatch(self, request, *args, **kwargs): edit_user = get_object_or_404(User, pk=self.kwargs['pk']) if ( request.user.has_perm('gym.manage_gym') or request.user.has_perm('gym.gym_trainer') ) and edit_user.userprofile.gym_id != request.user.userprofile.gym_id: return HttpResponseForbidden() # NO check: is the target user more privileged than the requeste

osv:PYSEC-2026-4316

wger

Fixed versions: Not specified

Complete source record
{
  "id": "55a8428017a1e35146389292d70718706f7f0f9ef09066b3faf8b0142b424335",
  "url": "https://osv.dev/vulnerability/PYSEC-2026-4316",
  "method": "structured",
  "payload": {
    "kind": "advisory",
    "title": "wger: Trainer Privilege Escalation - Improper Privilege Management",
    "summary": "### Summary A user with only the `gym_trainer` permission can deactivate any account in the same gym, including `gym_manager` and `general_gym_manager` accounts. The `UserDeactivateView` grants access to anyone holding **any one** of `gym.manage_gym`, `gym.manage_gyms`, or `gym.gym_trainer` (OR logic via `WgerMultiplePermissionRequiredMixin`), and performs no privilege-hierarchy check to prevent a lower-privileged role from disabling a higher-privileged one. ### Details `UserDeactivateView` (file: `wger/core/views/user.py`, line 378) is configured with: ```python permission_required = ('gym.manage_gym', 'gym.manage_gyms', 'gym.gym_trainer') ``` `WgerMultiplePermissionRequiredMixin` (file: `wger/utils/generic_views.py`, line 48) treats this tuple as an OR check -- any single permission is sufficient: ```python class WgerMultiplePermissionRequiredMixin(PermissionRequiredMixin): def has_permission(self): for permission in self.get_permission_required(): if self.request.user.has_perm(permission): return True # <-- ANY one permission is enough return False ``` The `dispatch()` method only verifies same-gym membership: ```python def dispatch(self, request, *args, **kwargs): edit_user = get_object_or_404(User, pk=self.kwargs['pk']) if ( request.user.has_perm('gym.manage_gym') or request.user.has_perm('gym.gym_trainer') ) and edit_user.userprofile.gym_id != request.user.userprofile.gym_id: return HttpResponseForbidden() # NO check: is the target user more privileged than the requeste",
    "evidence": "osv:PYSEC-2026-4316",
    "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-4316",
      "aliases": [
        "CVE-2026-46434",
        "GHSA-x249-cx55-2h87",
        "PYSEC-2026-4316"
      ],
      "metrics": [
        {
          "type": "CVSS_V3",
          "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/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-4316.yaml"
          }
        }
      ],
      "modified": "2026-10-15T14:45:10.347690155Z",
      "provider": "osv",
      "references": [
        "https://github.com/wger-project/wger/security/advisories/GHSA-x249-cx55-2h87",
        "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-x249-cx55-2h87",
        "https://nvd.nist.gov/vuln/detail/CVE-2026-46434"
      ]
    },
    "related_cves": [
      {
        "value": "CVE-2026-46434",
        "evidence": "/aliases"
      }
    ],
    "security_relevant": true,
    "severity_evidence": "/severity/0/score"
  },
  "source_id": "core-osv",
  "document_id": 332650,
  "published_at": "2026-10-15T10:22:47.076447+00:00"
}