Feb 27, 2026·6 min read·4 visits
The wger API forgot to filter database queries by the current user. Authenticated attackers can query specific endpoints to dump the repetition configurations (sets, reps, workout structure) of all users on the instance.
A classic Insecure Direct Object Reference (IDOR) vulnerability in the wger Workout Manager allows authenticated users to access the repetition configurations of every other user on the platform. Due to a failure to filter API querysets by the requesting user, the application serves up the entire database's workout structure to anyone with a valid account.
Let's talk about wger (pronounced however you want, though I lean towards 'wager' that you'll skip leg day). It is the open-source, self-hostable answer to those subscription-heavy fitness apps that hold your data hostage. It manages your workouts, your nutrition, and your progress. It’s a fantastic tool for privacy-conscious gym rats who want to own their data.
But in versions up to 2.4, wger decided that 'community' meant 'radical transparency.' Imagine walking into a gym locker room where every single locker is wide open, and the personal diaries of every member are taped to the doors. That is essentially what happened here.
The vulnerability is a textbook Insecure Direct Object Reference (IDOR), or what the OWASP Top 10 now fancies calling Broken Object Level Authorization (BOLA). While the CVSS score is a modest 4.3 (because apparently, knowing how much your neighbor benches isn't 'Critical' infrastructure damage), the logic flaw represents a fundamental failure in API design. It turns a private workout tracker into a public broadcast system.
Where did the developers go wrong? They fell victim to the path of least resistance in the Django Rest Framework (DRF). DRF is powerful; it automates a lot of the heavy lifting involved in serializing database objects into JSON. But it also gives you enough rope to hang yourself if you aren't paying attention to scoping.
In a proper multi-tenant application (or multi-user, in this case), every single database query regarding user data should have a WHERE clause. You want SELECT * FROM workouts WHERE user_id = current_user. If you forget that WHERE clause, the database happily returns everything.
The flaw in wger was located in the RepetitionsConfigViewSet and MaxRepetitionsConfigViewSet. These views are responsible for serving up the nitty-gritty details of a workout—how many reps, what configuration, etc. Instead of filtering the queryset based on self.request.user, the code simply called .all(). It’s the SQL equivalent of shouting 'EVERYONE!' when asked who is in the room.
Let’s look at the code. It is almost painful in its simplicity. This is the vulnerable implementation found in wger/manager/api/views.py:
# VULNERABLE CODE
class RepetitionsConfigViewSet(viewsets.ModelViewSet):
def get_queryset(self):
# "Here, have everything. I trust you."
return RepetitionsConfig.objects.all()
class MaxRepetitionsConfigViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return MaxRepetitionsConfig.objects.all()See that objects.all()? That is the sound of privacy dying. There is no check to see if the user requesting the data actually owns it. If you have a valid token, you get the list.
Now, let's look at the fix introduced in commit 1fda5690b35706bb137850c8a084ec6a13317b64. The developers switched to a custom base class that likely enforces ownership, and they explicitly filtered the query:
# PATCHED CODE
class RepetitionsConfigViewSet(WgerOwnerObjectModelViewSet):
def get_queryset(self):
# "Who are you again? Oh, right. Here is YOUR stuff."
return RepetitionsConfig.objects.filter(
slot_entry__slot__day__routine__user=self.request.user
)The patch creates a join chain (slot_entry -> slot -> day -> routine -> user) to verify that the config actually belongs to the person asking for it. This is how it should have been from day one.
Exploiting this does not require a complex buffer overflow or some wizard-level ROP chain. It requires curl and a registered account. Since wger is often deployed with open registration, the barrier to entry is non-existent.
Step 1: Get in the door. Register an account on the target instance. Log in to get your API token.
Step 2: Ask politely. Send a GET request to the vulnerable endpoint. The API uses sequential IDs (because of course it does), so enumeration is trivial, but you don't even need to iterate IDs here—the list endpoint returns the collection.
# The "Hacker" Tooling
curl -H "Authorization: Token <YOUR_TOKEN>" \
https://wger-instance.com/api/v2/repetitions-config/Step 3: Parse the loot. The server responds with a JSON array containing every repetition configuration in the database. You now have the workout structure for every user: specific exercises, rep counts, and routine data. If the IDs are sequential, you can map these configurations back to specific routines and potentially correlate them with specific users if other IDORs exist (which, let's be honest, they probably do).
I can hear the detractors now: "So what? They know I do 3 sets of 10. Big deal." And sure, this isn't a dump of credit card numbers or medical records (though health data is protected class in many jurisdictions).
However, consider the context. People use these apps to track progress, physical limitations, and rehabilitation routines. In a competitive environment (say, a sports team using a self-hosted instance), leaking the exact training regimen of your athletes to a rival is a strategic disaster.
Furthermore, this is a privacy violation. Users expect their data to be siloed. If I can see your workout configs, I can infer your activity levels, your schedule, and potentially your location habits if the metadata is rich enough. It is a breach of trust, plain and simple.
The remediation is straightforward, but it requires diligence. If you are running wger, you need to pull the latest docker image or update your source code immediately. The fix is live in the main branch.
For developers, the lesson is clear: Never trust the client, and never trust the default queryset.
get_queryset in DRF to filter by request.user unless the data is explicitly public.CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
wger wger-project | <= 2.4 | commit 1fda569 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-639 |
| Attack Vector | Network (API) |
| CVSS v3.1 | 4.3 (Medium) |
| Impact | Information Disclosure |
| Exploit Status | PoC Available |
| Authentication | Required (Low Privilege) |