Table of Contents
- Key Highlights
- Introduction
- How the code allowed the exposure
- Why object-level authorization matters in APIs
- Proof-of-concept: reproducing the behavior
- What data was exposed and why it matters
- Root cause analysis: how the mistake happened
- The straightforward fix
- Tests to add or update
- Hardening patterns for Django REST APIs
- Operational mitigations and response steps for deployed systems
- Preventing similar mistakes in development workflows
- Designing APIs to minimize enumeration risk
- Example: incorporating object-level permissions and UUIDs
- Real-world parallels and why vigilance matters
- Security lifecycle recommendations for maintainers
- Communicating the fix to users
- Checklist for code reviewers of API changes
- FAQ
Key Highlights
- Two Django REST viewsets (RepetitionsConfigViewSet and MaxRepetitionsConfigViewSet) returned every user's configuration because get_queryset() used .all() instead of filtering by the authenticated user.
- Any registered account could enumerate slot and repetition settings for all routines; open registration and sequential IDs made enumeration trivial.
- The fix is to apply the same user-based filter used by sibling viewsets or introduce proper object-level authorization; automated tests and rate limiting reduce the risk of recurrence.
Introduction
A security review of the wger project discovered a broken object‑level authorization (BOLA) vulnerability that allowed any authenticated user to read other users' repetition configuration data. Two viewsets exposed configuration objects by returning .all() from their get_queryset() methods rather than restricting results to objects owned by the requesting user. The problem is isolated and straightforward to fix, but it demonstrates how small inconsistencies in API code can lead to unwarranted data exposure across an otherwise well-structured codebase.
The exposed data does not include credentials, but does reveal workout structure: slot entry identifiers, iteration values, operations, step counts, repeat flags and requirements JSON. For privacy-conscious users or closed communities, this is a material leak of personal exercise program structure and could enable profiling or automated scraping at scale.
The following sections explain the technical root cause, reproduce the behavior with a proof-of-concept, describe the impact in practical terms, and lay out remediation and long-term hardening advice for developers maintaining Django REST Framework (DRF) APIs.
How the code allowed the exposure
At the heart of this issue are two viewsets in the project’s API layer:
- RepetitionsConfigViewSet
- MaxRepetitionsConfigViewSet
Both are ModelViewSet subclasses. Instead of returning only records tied to the requesting user, each get_queryset() returned all objects for the model:
# VULNERABLE
class RepetitionsConfigViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return RepetitionsConfig.objects.all()
class MaxRepetitionsConfigViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return MaxRepetitionsConfig.objects.all()
Nearby viewsets for other configuration types (for example WeightConfigViewSet, SetsConfigViewSet, RestConfigViewSet and their Max variants) used an explicit filter that connected the configuration object to the owning user through the slot/entry/routine relationship chain:
# CORRECT — how it should work
def get_queryset(self):
return WeightConfig.objects.filter(
slot_entry__slot__day__routine__user=self.request.user
)
Because the two repetition-related viewsets were missing this filter, any authenticated user calling the list endpoints received the complete set of repetition configurations stored in the database.
The omission is narrow: the rest of the file applies the correct filtering and permissions. The error appears to be a simple oversight rather than a systemic failure of access control patterns in the project. That said, such oversights are the common root cause of BOLA issues.
Why object-level authorization matters in APIs
APIs often expose lists and detail endpoints for resources connected to user accounts. When authorization is applied only at the endpoint level (for example, “you must be authenticated to access this endpoint”), the developer must still enforce that the collection returned includes only the objects the caller is allowed to see.
Broken object-level authorization — frequently labeled IDOR (insecure direct object reference) when tied to predictable identifiers — arises when the link between the object and the authenticated principal is not enforced. The result: a caller can access or enumerate objects belonging to other users.
The severity of such leaks depends on context. Exposing profile pictures or workout settings may seem low risk compared with credential theft, but the data can still be sensitive. Workout regimen structure can reveal lifestyle and health patterns, training levels, and even schedule regularities. For closed communities, exposing members’ program details could undermine privacy commitments.
The OWASP API Security Top 10 identifies this class of vulnerability as API1:2019 — Broken Object Level Authorization. The wger finding fits that pattern: a missing owner filter allowed cross-user access to application objects.
Proof-of-concept: reproducing the behavior
The vulnerability is trivial to reproduce when an attacker has a valid account. With a token or session cookie and knowledge of the API endpoints, a single GET request returns all stored repetition configurations.
A minimal Python PoC using the requests library:
import requests
BASE = "http://localhost"
headers = {"Authorization": "Token YOUR_TOKEN"} # any registered user
r = requests.get(f"{BASE}/api/v2/repetitions-config/", headers=headers)
print(r.json()) # returns ALL users' repetition configs, not just your own
r = requests.get(f"{BASE}/api/v2/max-repetitions-config/", headers=headers)
print(r.json()) # same — all users' max repetition configs
Two factors make enumeration especially easy in typical deployments:
- Registration is open by default: attackers can create accounts at scale without manual approvals.
- Sequential numeric IDs: predictable identifiers let attackers map ranges (e.g., 1..N), stitch together related objects, and harvest configuration structures automatically.
A determined actor can paginate through the API responses, persist the returned JSON, and bulk-analyze routine patterns. Even without authentication, the presence of an open registration flow means creating an account for the purpose of data harvesting requires only minimal effort.
What data was exposed and why it matters
The exposed fields include the core attributes of repetition and max-repetition configuration objects. Typical fields observed in similar models are:
- slot_entry IDs (links to slot entries within a workout)
- iteration values (how many repetitions per set, exceptions, etc.)
- operations (increment/decrement/match logic)
- step counts and repeat flags (how a sequence or set repeats)
- requirements JSON (conditional logic that may include structured rules)
Practical implications
- Privacy: Users may consider their custom routines private. Access to structured routines enables profiling of fitness level and training emphasis.
- Intellectual property: Trainers or paid programs stored as templates could be scraped and republished.
- Automation: Attackers could reconstruct workout workflows and build tools that mimic user behavior or scrape program libraries.
- Correlation attacks: Combined with other exposed profile data (name, email, timestamps) the configuration can enhance deanonymization and targeted social engineering.
The leak does not directly expose passwords or personal financial information. However, the ability to enumerate resource IDs and relationship graphs can be leveraged in follow-up attacks or combined with other vulnerabilities for privilege escalation.
Root cause analysis: how the mistake happened
The codebase applies a correct owner-based filter in many places; the repetition endpoints diverged only by a single, seemingly minor omission. Causes for this class of defect typically include:
- Copy/paste errors: developer duplicated a viewset and removed the filter while adapting for a new model.
- Incomplete review: the presence of correct filters in sibling viewsets suggests manual review may have missed the two cases.
- Over-reliance on endpoint-level permissions: developers sometimes assume authentication alone suffices without remembering to restrict objects returned.
- No unit or integration tests for access control: absent tests that assert "list returns only current user's objects", regressions slip through.
In Django REST Framework, get_queryset() is the canonical place to apply collection-level scoping. Failing to use the request context there leaves an endpoint susceptible to cross-account data leakage. Additionally, if the viewset relies on default permissions but does not implement object-level permissions via has_object_permission, the same mistake can appear in both list and retrieve flows.
The straightforward fix
The correct approach is to apply the same owner filter used across related viewsets. For both vulnerable viewsets, change get_queryset() to filter by the authenticated user:
def get_queryset(self):
return RepetitionsConfig.objects.filter(
slot_entry__slot__day__routine__user=self.request.user
)
Do the same for MaxRepetitionsConfig:
def get_queryset(self):
return MaxRepetitionsConfig.objects.filter(
slot_entry__slot__day__routine__user=self.request.user
)
This enforces that list endpoints return only configuration objects associated with the user's routines. For extra defense, pair this with object-level permission checks. For example, implement a custom permission class that verifies ownership in has_object_permission for retrieve, update, and delete operations.
Sample permission class skeleton:
from rest_framework import permissions
class IsRoutineOwner(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
# obj is a config object with a relation to routine via slot_entry chain
return obj.slot_entry.slot.day.routine.user == request.user
Register the permission for the viewsets:
class RepetitionsConfigViewSet(viewsets.ModelViewSet):
permission_classes = [permissions.IsAuthenticated, IsRoutineOwner]
...
Edge cases to consider: superuser/administrative roles may need broader access; add explicit checks or separate admin endpoints with tighter audit controls.
Tests to add or update
A robust test suite detects similar regressions quickly. Tests to add:
-
Unit tests for get_queryset():
- Create two users; create config objects tied to each; assert that user A’s call to list returns only A’s objects.
- Test that retrieve on an object belonging to another user returns 403/404 depending on chosen behavior.
-
Integration tests for the full viewset:
- Test list, retrieve, create, update, and delete behaviors across ownership boundaries.
- Include tests for batch endpoints and pagination to ensure scoping applies to all pages.
-
Permission tests:
- Assert that the custom permission class blocks access when ownership fails.
- Verify administrators can access objects if intended, and that logs are produced.
-
Fuzz and property tests:
- Generate random IDs and assert the API never leaks objects for unauthorized users.
Example pytest-style assertion for list scoping:
def test_list_only_returns_owner_configs(api_client, user_factory, config_factory):
user1 = user_factory()
user2 = user_factory()
api_client.force_authenticate(user=user1)
config1 = config_factory(owner=user1)
config2 = config_factory(owner=user2)
response = api_client.get('/api/v2/repetitions-config/')
ids = {item['id'] for item in response.json()['results']}
assert config1.id in ids
assert config2.id not in ids
Make these tests part of continuous integration to stop regressions at merge time.
Hardening patterns for Django REST APIs
The vulnerability illustrates repeatable lessons for multi-tenant or user-scoped APIs. The following patterns reduce the risk of BOLA and related leaks:
- Always bind collections to the authenticated user in get_queryset() or use view-level mixins that enforce scoping.
- Prefer explicit owner relationships at the database level (ForeignKey to User) where appropriate to simplify filtering.
- Implement object-level permissions using has_object_permission to guard detail endpoints and method-specific operations.
- Use serializers that expose only the fields the caller should see; treat nested relations with caution.
- Avoid exposing internal numeric IDs directly if not necessary. Use UUIDs for public identifiers to make enumeration more difficult.
- Apply rate limiting to list endpoints and registration endpoints to slow down automated enumeration.
- Log access to sensitive endpoints and monitor unusual patterns: high-volume list requests, repeated pagination across IDs, and spikes from single accounts.
- Consider field-level redaction for sensitive attributes in a serializer based on the request user or role.
None of these measures alone eliminates risk, but layered defenses make exploitation harder and detection likelier.
Operational mitigations and response steps for deployed systems
If a vulnerable endpoint is already deployed, follow a measured response:
- Patch the code: apply the owner-based filter and deploy as quickly as possible through your standard release pipeline.
- Add tests and CI gates: prevent regressions with test coverage for access control.
- Rotate tokens or take targeted actions only if you know tokens were abused; broad token revocation may be disruptive. Instead, monitor for anomalous access.
- Search logs for suspicious calls:
- Identify calls to /repetitions-config/ and /max-repetitions-config/ from accounts other than expected owners.
- Look for high-volume pagination or full-table enumeration patterns.
- Notify stakeholders: maintainers and, where required by policy, affected users. For open-source projects, follow the established security advisory process (e.g., GHSA) and coordinate timing of disclosure and patch release.
- Rate-limiting and WAF rules: temporarily harden the API surface by enforcing stricter rate limits on the impacted endpoints and blocking abusive IP patterns if abuse is detected.
- Roll out enhanced logging and auditing to detect any subsequent access attempts.
Avoid public statements that provide exploit details before users can update. Coordinate release notes to communicate the nature of the fix and actions maintainers applied.
Preventing similar mistakes in development workflows
Small oversights survive where the development lifecycle lacks automated safety nets. Adopt the following practices:
- Code review checklists: include an explicit item “Does get_queryset() or the serializer restrict objects to the requesting user?” Make this a mandatory review question for APIs with user-scoped data.
- Security-focused code reviews: involve someone with security experience when PRs affect authentication, authorization, or data access code.
- Automated static analysis and linters: implement tools that flag patterns like Model.objects.all() usage in view code. While not foolproof, such heuristics catch obvious cases.
- Threat modeling during design: for new API endpoints, list the trust boundaries and object ownership models. Decide up front whether objects are per-user, per-organization, or public.
- Continuous integration checks: require tests that validate access rules before merge. Consider contract tests that validate the API’s authorization semantics from the consumer’s perspective.
- Developer education: teach common attack patterns like IDOR and why they matter in the codebase context.
These practices require modest upfront investment but pay large dividends by preventing regressions and building security into routine development.
Designing APIs to minimize enumeration risk
Even when object-level authorization is correct, predictable identifiers and open registration increase the ease of mass harvesting. Mitigation strategies:
- Use non-predictable public identifiers for resources that could be enumerated. UUID4 or cryptographic IDs prevent easy numeric scanning.
- Keep internal numeric primary keys private; return public IDs based on a separate field.
- Protect registration endpoints with rate limits, CAPTCHA, or email verification when anonymity is not required.
- Require elevated privileges or secondary authentication for endpoints that expose schema-like information or templates used by multiple users.
- Apply return-size limits and sampling for large list endpoints. When possible, return minimal metadata in lists and require explicit detail requests for full records.
- Add a per-account rate limit and behavioral monitoring to detect scraping. If enumeration spikes, temporarily tighten limits or require additional verification.
These changes reduce the value and feasibility of bulk enumeration attacks.
Example: incorporating object-level permissions and UUIDs
A combined approach offers a durable defense. Steps:
- Change the models to include a public UUID field:
from django.db import models
import uuid
class RepetitionsConfig(models.Model):
public_id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
slot_entry = models.ForeignKey(SlotEntry, ...)
# other fields...
- Always filter collections by the user:
def get_queryset(self):
return RepetitionsConfig.objects.filter(slot_entry__slot__day__routine__user=self.request.user)
- Use object-level permissions:
class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return obj.slot_entry.slot.day.routine.user == request.user
return obj.slot_entry.slot.day.routine.user == request.user
- Expose only public_id as the identifier in APIs and avoid returning internal numeric IDs. This requires adjusting serializers:
class RepetitionsConfigSerializer(serializers.ModelSerializer):
id = serializers.UUIDField(source='public_id', read_only=True)
class Meta:
model = RepetitionsConfig
fields = ('id', 'iteration', 'operation', 'steps', ...)
This mix reduces enumeration risk and makes accidental leaks less useful to attackers.
Real-world parallels and why vigilance matters
Similar BOLA/IDOR mistakes have been responsible for numerous data exposures across web and mobile applications. The root cause is not lack of effort but a focus misalignment: authentication is often implemented first, and application-level object scoping can be forgotten or inconsistently applied. Public bug bounty disclosures regularly list “list endpoint returns all users’ objects” as a recurring finding.
Open-source projects with permissive registration and low friction contribute to the ease of exploitation. For projects that host private or paid content, the stakes are higher: scraped trainer routines, repetitive exposure of subscription-only templates, or even a competitor harvesting program structures can result in financial and reputational damage.
Maintainers should treat API access control as code that must be tested and reviewed, not merely configured.
Security lifecycle recommendations for maintainers
Short-term
- Patch the vulnerable viewsets immediately with the owner filter.
- Add tests that confirm access control.
- Release a security advisory and coordinate disclosure if the project tracks advisories publicly.
Medium-term
- Audit similar viewsets for the same omission.
- Add unit and integration tests for all API endpoints that expose user-scoped resources.
- Add CI checks and static analysis that flags potential ownerless queries.
Long-term
- Adopt UUIDs for public-facing resource identifiers.
- Integrate periodic security reviews and a responsible disclosure program.
- Enhance monitoring and detection for scraping or enumeration activity.
These steps reduce both the window of exposure and the probability of reintroduction of similar bugs.
Communicating the fix to users
For projects with installed user bases, communication must be measured:
- Describe the nature of the issue at a high level: which endpoints were affected and what type of data may have been exposed.
- Confirm the patch has been applied and provide a version number or commit hash.
- Explain any remediation actions taken (logging, rate-limiting, targeted token rotation) without listing exploit details that could enable re-testing on unpatched instances.
- Provide guidance on whether users need to take action (usually none for this class of non-credential leak), but offer channels for reporting suspicious account activity.
- If coordinated disclosure or advisories exist (GHSA or CVE), link to the advisory for maintainers running self-hosted instances.
Clarity and transparency build trust; avoid alarmist language while being precise about what changed.
Checklist for code reviewers of API changes
Adopt a concrete checklist to catch this class of mistake during review:
- Does the viewset's get_queryset() scope results to the request user or organization?
- Are object-level permissions applied for retrieve/update/delete operations?
- Are serializers exposing only intended fields for the caller’s role?
- Are public identifiers randomized or protected against simple enumeration?
- Do tests exist that assert unauthorized users cannot access or list the resource?
- Has the change been stress-tested for pagination, filtering, and large result sets?
- Is there centralized logging for accesses to sensitive endpoints?
Make answering these questions mandatory for API-related reviews.
FAQ
Q: Which endpoints were affected? A: The RepetitionsConfig and MaxRepetitionsConfig list endpoints returned all objects because get_queryset() used .all() rather than filtering by the authenticated user. The corresponding endpoints for weight, sets, rest, and other config types used correct owner filters.
Q: Did the vulnerability expose passwords or payment details? A: No. The exposed data consisted of repetition configuration fields (slot entry references, iteration values, step counts, operations, repeat flags, and requirements JSON). Credentials and payment details were not part of these models.
Q: Could a non-registered attacker access the data? A: No. The endpoints required authentication. However, registration is open by default in many deployments, so an attacker can create an account to exploit the endpoints. Rate limits and registration protections can slow this down.
Q: How easy was enumeration? A: Very easy. Two factors made it trivial: open registration and sequential numeric IDs. An authenticated account could paginate responses or iterate predictable IDs to reconstruct configuration graphs.
Q: What is the recommended fix? A: Apply a user-based filter to get_queryset() for both viewsets to return only objects owned by the requesting user. Add object-level permission checks and include tests that validate list and retrieve behaviors. Example:
def get_queryset(self):
return RepetitionsConfig.objects.filter(
slot_entry__slot__day__routine__user=self.request.user
)
Q: Do maintainers need to rotate tokens or take user-facing action? A: Rotating tokens is usually unnecessary unless there is evidence of abuse tied to specific tokens. Priority actions are code patch, increased logging, and search of access logs for suspicious enumeration. Notify users if the project’s disclosure policy requires it.
Q: How can I detect if my deployment is vulnerable? A: From an authenticated account, request the two endpoints:
- /api/v2/repetitions-config/
- /api/v2/max-repetitions-config/ If the responses include objects that do not belong to the authenticated user, the deployment is vulnerable. Additionally, inspect the viewset code for the get_queryset() calls and search for unfiltered Model.objects.all() usage.
Q: Are there longer-term architectural changes recommended? A: Yes. Use non-predictable public IDs (UUIDs), implement consistent object-level permissions, add automated tests for access control, and apply rate limiting to reduce automated scraping risk.
Q: Where was this reported? A: The issue has been tracked in the project’s security advisory (GHSA-xf68-8hjw-7mpm). Maintainers should consult the advisory for timeline and remediation details.
Q: What defensive measures will prevent similar bugs? A: Mandatory code review questions about get_queryset() scoping, CI tests that validate authorization, static analysis rules that flag unscoped queries in view code, and threat modeling for API endpoints all reduce the risk of reintroduction.
This incident is a reminder that consistent application of authorization logic is essential. Authentication gates prevent anonymous access but do not guarantee object-level correctness. Small omissions in view logic can produce wide-ranging data exposures; the fix is straightforward, tests prevent regression, and a layered approach to API design reduces the risk of future oversights.