GHSA-xppv-4jrx-qf8m: wger has Broken Access Control in Global Gym Configuration Update Endpoint
Summarywger 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 endpointThe config URLs map as follows.File: `wger/config/urls.py`This resolves to:`/config/gym-config/edit`Root causeThe view declares a permission but does not enforce itFile: `wger/config/views/gym_config.py`The permission string exists, but `WgerFormMixin` does not check `permission_required`.The project’s permission mixin exists but is not usedFile: `wger/utils/generic_views.py``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 ownerFile: `wger/utils/generic_views.py`File: `wger/config/models/gym_config.py`Because `GymConfig` does not implement `get_owner_object()`, `WgerFormMixin` catches `AttributeError` and sets `owner_object = False`, skipping any access restriction.Security impactThis is not a cosmetic setting: `GymConfig.save()` performs installation-wide side effects.File: `wger/config/models/gym_config.py`On deployments with multiple gyms, this allows a low-privileged user to tamper with tenant assignment defaults, affecting new registrations and bulk-updating existing users lacking a gym. This permits unauthorized modification of installation-wide state and bulk updates to other users’ records, violating the intended administrative trust boundary.Proof of concept (local verification)Environment: local docker compose stack, accessed via `http://127.0.0.1:8088/en/`.Observed behaviorAn unauthenticated user can reach the endpoint via GET; POST requires authentication and redirects to login.
An authenticated low-privileged user can submit the form and change the global singleton. After the save, the application redirects to `success_url = reverse_lazy('gym:gym:list')` (e.g. `/en/gym/list`), which is permission-protected; therefore the browser may display a “Forbidden” page even though the global update already succeeded.DB evidence (before/after)Before submission:After a low-privileged user submitted the form setting `default_gym` to gym id `1`:Recommended fixEnsure permission enforcement runs before the form dispatch.Using the project mixin (order matters):Alternatively, use Django’s `PermissionRequiredMixin` (and `LoginRequiredMixin`) directly.ConclusionThe view explicitly declares permission_required = 'config.change_gymconfig', which demonstrates developer intent that this action be restricted. The fact that it is not enforced constitutes improper access control regardless of perceived business impact.
CVSS v3.1: 7.6