🛡️ CVE-2026-40474 — wger

🟠 CVSS 8.0 — High ⚠️ Exploit Public CWE-284 OSV
8.0
CVSS Score
0 Low4 Medium7 High9 Critical10

Description

wger has Broken Access Control in Global Gym Configuration Update Endpoint

Summary

wger exposes a global configuration edit endpoint at /config/gym-config/edit implemented by GymConfigUpdateView. The view declares permission_required = 'config.change_gymconfig' but does not enforce it because it inherits WgerFormMixin (ownership-only checks) instead of the project’s permission-enforcing mixin (WgerPermissionMixin) .

The edited object is a singleton (GymConfig(pk=1)) and the model does not implement get_owner_object(), so WgerFormMixin skips ownership enforcement. As a result, a low-privileged authenticated user can modify installation-wide configuration and trigger server-side side effects in GymConfig.save().

This is a vertical privilege escalation from a regular user to privileged global configuration control.

The application explicitly declares permission_required = 'config.change_gymconfig', demonstrating that the action is intended to be restricted; however, this requirement is never enforced at runtime.

Affected endpoint

The config URLs map as follows.

File: wger/config/urls.py

```python

patterns_gym_config = [

path('edit', gym_config.GymConfigUpdateView.as_view(), name='edit'),

]

urlpatterns = [

path(

'gym-config/',

include((patterns_gym_config, 'gym_config'), namespace='gym_config'),

),

]

```

This resolves to:

/config/gym-config/edit

Root cause

The view declares a permission but does not enforce it

File: wger/config/views/gym_config.py

```python

class GymConfigUpdateView(WgerFormMixin, UpdateView):

model = GymConfig

fields = ('default_gym',)

permission_required = 'config.change_gymconfig'

success_url = reverse_lazy('gym:gym:list')

title = gettext_lazy('Edit')

def get_object(self):

return GymConfig.objects.get(pk=1)

```

The permission string exists, but WgerFormMixin does not check permission_required.

The project’s permission mixin exists but is not used

File: wger/utils/generic_views.py

```python

class WgerPermissionMixin:

permission_required = False

login_required = False

def dispatch(self, request, *args, **kwargs):

if self.login_required or self.permission_required:

if not request.user.is_authenticated:

return HttpResponseRedirect(

reverse_lazy('core:user:login') + f'?next={request.path}'

)

if self.permission_required:

has_permission = False

if isinstance(self.permission_required, tuple):

for permission in self.permission_required:

if request.user.has_perm(permission):

has_permission = True

elif request.user.has_perm(self.permission_required):

has_permission = True

if not has_permission:

return HttpResponseForbidden('You are not allowed to access this object')

return super(WgerPermissionMixin, self).dispatch(request, *args, **kwargs)

```

GymConfigUpdateView does not inherit this mixin, so none of the login/permission logic runs.

The mixin that *is* used performs only ownership checks, and GymConfig has no owner

File: wger/utils/generic_views.py

```python

class WgerFormMixin(ModelFormMixin):

def dispatch(self, request, *args, **kwargs):

self.kwargs = kwargs

self.request = request

if self.owner_object:

owner_object = self.owner_object['class'].objects.get(pk=kwargs[self.owner_object['pk']])

else:

try:

owner_object = self.get_object().get_owner_object()

except AttributeError:

owner_object = False

if owner_object and owner_object.user != self.request.user:

return HttpResponseForbidden('You are not allowed to access this object')

return super(WgerFormMixin, self).dispatch(request, *args, **kwargs)

```

File: wger/config/models/gym_config.py

```python

class GymConfig(models.Model):

default_gym = models.ForeignKey(

Gym,

verbose_name=_('Default gym'),

# ...

null=True,

blank=True,

on_delete=models.CASCADE,

)

# No get_owner_object() method

```

Because GymConfig does not implement get_owner_object(), WgerFormMixin catches AttributeError and sets owner_object = False, skipping any access restriction.

Security impact

This is not a cosmetic setting: GymConfig.save() performs installation-wide side effects.

File: wger/config/models/gym_config.py

```python

def save(self, *args, **kwargs):

if self.default_gym:

UserProfile.objects.filter(gym=None).update(gym=self.default_gym)

for profile in UserProfile.objects.filter(gym=self.default_gym):

user = profile.user

if not is_any_gym_admin(user):

try:

user.g

How this vulnerability can be exploited

This issue can be reached over the network, attack complexity is low, an attacker needs low-level privileges on the target. No user interaction is required. The scope is unchanged, so the impact stays within the vulnerable component. Rated impact: confidentiality low, integrity high, availability low.

Weakness class

CVE-2026-40474 is classified as CWE-284: Improper Access Control. The software does not restrict an action to the actors that should be allowed to perform it.

Affected software

CVE-2026-40474 is recorded against 1 package.

  • wger

Timeline and source

Published on 16 April 2026 and last revised on 13 July 2026. A public exploit is known to exist, which raises the urgency of patching considerably. A vendor advisory or fix has been published. Record sourced from OSV.

References

github.com (Web)
nvd.nist.gov (Advisory)
github.com (Web)
github.com (Package)
github.com (Web)

Details

Severity HIGH
CVSS Score 8.0
CVSS Vector CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:L
CWE CWE-284
Public Exploit ⚠️ Yes
Source OSV
Published 2026-04-16
Updated 2026-08-12
Modified 2026-07-13

Affected Packages

Software From version Fixed in
wger

Similar Threats

Exploit Protection

Are you running wger?

CVE-2026-40474 carries CVSS 8.0 High rating and a public exploit already exists. BotEraser checks your installation against this and other known CVE records, and blocks IPs associated with exploit activity.

Check My Site For CVE-2026-40474 →

No credit card required  ·  Results in minutes

ⓘ Data Notice: The information presented above has been compiled from publicly available internet sources. Boteraser aggregates this data solely for informational purposes and does not independently classify, evaluate, or endorse any findings about the vulnerabilities listed. The accuracy and completeness of this information is the sole responsibility of the original publishers. Boteraser and its operators accept no liability for any decisions made based on this data.