VYPR
Low severityNVD Advisory· Published Feb 26, 2026· Updated Mar 3, 2026

wger: IDOR via user-unscoped cache keys on routine API actions exposes workout data

CVE-2026-27838

Description

wger is a free, open-source workout and fitness manager. Five routine detail action endpoints check a cache before calling self.get_object(). In versions up to and including 2.4, ache 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. Commit e964328784e2ee2830a1991d69fadbce86ac9fbf contains a patch for the issue.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

wger workout manager has an IDOR vulnerability due to cache keys lacking user IDs, allowing attackers to retrieve other users' routine data from cache.

The vulnerability resides in five routine detail API endpoints of wger, a self-hosted fitness manager. These endpoints check a cache before performing ownership verification via self.get_object(). The cache keys are constructed using only the routine's primary key (pk) without any user identifier, meaning cached responses are shared across users [1]. This is an insecure direct object reference (IDOR), as an authenticated attacker can retrieve another user's routine data if the victim has previously accessed the same routine via the API [4].

Exploitation requires an attacker with a registered account to know or guess the routine pk that a victim has accessed. When the victim requests an affected endpoint (e.g., /api/v2/routine/{pk}/structure/), the response is cached for one month. An attacker then requests the same endpoint with the same pk; the cache returns the victim's data before any ownership check is performed [4]. No authentication bypass is needed beyond having a valid account.

The impact is exposure of private workout data, including routine structure, exercise sequences, training logs, and statistics [4]. An attacker can infer a victim's exercise habits and progress, compromising user privacy. The vulnerability does not allow data modification but enables unauthorized access to sensitive information.

Mitigation is provided by commit e964328784e2ee2830a1991d69fadbce86ac9fbf, which includes the requesting user's ID in the cache key [3]. The fix ensures that each user's cached data is isolated. Users are advised to update wger to a version containing this commit or apply the patch manually [4].

AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
wgerPyPI
<= 2.1

Affected products

2
  • Wger/Wgerllm-fuzzy
    Range: <=2.4
  • wger-project/wgerv5
    Range: <= 2.4

Patches

1
e964328784e2

Refactor cache keys to include user ID for the routine API endpoints

https://github.com/wger-project/wgerRoland GeiderFeb 24, 2026via ghsa
4 files changed · +24 29
  • wger/manager/api/views.py+5 11 modified
    @@ -128,10 +128,7 @@ def date_sequence_display_mode(self, request, pk):
             """
             Return the day sequence of the routine
             """
    -        # profiler = cProfile.Profile()
    -        # profiler.enable()
    -
    -        cache_key = CacheKeyMapper.routine_api_date_sequence_display_key(pk)
    +        cache_key = CacheKeyMapper.routine_api_date_sequence_display_key(pk, request.user.id)
             cached_data = cache.get(cache_key)
             if cached_data is not None:
                 return Response(cached_data)
    @@ -142,17 +139,14 @@ def date_sequence_display_mode(self, request, pk):
             ).data
             cache.set(cache_key, out, settings.WGER_SETTINGS['ROUTINE_CACHE_TTL'])
     
    -        # profiler.disable()
    -        # profiler.dump_stats("profile_results.prof")
    -
             return Response(out)
     
         @action(detail=True, url_path='date-sequence-gym')
         def date_sequence_gym_mode(self, request, pk):
             """
             Return the day sequence of the routine
             """
    -        cache_key = CacheKeyMapper.routine_api_date_sequence_gym_key(pk)
    +        cache_key = CacheKeyMapper.routine_api_date_sequence_gym_key(pk, request.user.id)
             cached_data = cache.get(cache_key)
             if cached_data is not None:
                 return Response(cached_data)
    @@ -167,7 +161,7 @@ def structure(self, request, pk):
             """
             Return the full object structure of the routine.
             """
    -        cache_key = CacheKeyMapper.routine_api_structure_key(pk)
    +        cache_key = CacheKeyMapper.routine_api_structure_key(pk, request.user.id)
             cached_data = cache.get(cache_key)
             if cached_data is not None:
                 return Response(cached_data)
    @@ -181,7 +175,7 @@ def logs(self, request, pk):
             """
             Returns the logs for the routine
             """
    -        cache_key = CacheKeyMapper.routine_api_logs(pk)
    +        cache_key = CacheKeyMapper.routine_api_logs(pk, request.user.id)
             cached_data = cache.get(cache_key)
             if cached_data is not None:
                 return Response(cached_data)
    @@ -195,7 +189,7 @@ def stats(self, request, pk):
             """
             Returns the logs for the routine
             """
    -        cache_key = CacheKeyMapper.routine_api_stats(pk)
    +        cache_key = CacheKeyMapper.routine_api_stats(pk, request.user.id)
             cached_data = cache.get(cache_key)
             if cached_data is not None:
                 return Response(cached_data)
    
  • wger/manager/helpers.py+5 4 modified
    @@ -186,12 +186,13 @@ def reset_routine_cache(instance: Routine, structure: bool = True):
         """Resets all caches related to a routine"""
     
         cache.delete(CacheKeyMapper.routine_date_sequence_key(instance.id))
    -    cache.delete(CacheKeyMapper.routine_api_date_sequence_display_key(instance.id))
    -    cache.delete(CacheKeyMapper.routine_api_date_sequence_gym_key(instance.id))
    -    cache.delete(CacheKeyMapper.routine_api_stats(instance.id))
    +    cache.delete(CacheKeyMapper.routine_api_date_sequence_display_key(instance.id, instance.user_id))
    +    cache.delete(CacheKeyMapper.routine_api_date_sequence_gym_key(instance.id, instance.user_id))
    +    cache.delete(CacheKeyMapper.routine_api_logs(instance.id, instance.user_id))
    +    cache.delete(CacheKeyMapper.routine_api_stats(instance.id, instance.user_id))
     
         if structure:
    -        cache.delete(CacheKeyMapper.routine_api_structure_key(instance.id))
    +        cache.delete(CacheKeyMapper.routine_api_structure_key(instance.id, instance.user_id))
     
         if instance.pk:
             for day in instance.days.all():
    
  • wger/manager/signals.py+2 2 modified
    @@ -77,14 +77,14 @@ def handle_config_change(sender, instance: AbstractChangeConfig, **kwargs):
     def handle_workout_log_change(sender, instance: WorkoutLog, **kwargs):
         update_activity_cache(sender, instance, **kwargs)
         if instance.routine:
    -        cache.delete(CacheKeyMapper.routine_api_logs(instance.routine.id))
    +        cache.delete(CacheKeyMapper.routine_api_logs(instance.routine.id, instance.user_id))
             reset_routine_cache(instance.routine, structure=False)
     
     
     def handle_workout_session_change(sender, instance: WorkoutSession, **kwargs):
         update_activity_cache(sender, instance, **kwargs)
         if instance.routine:
    -        cache.delete(CacheKeyMapper.routine_api_logs(instance.routine.id))
    +        cache.delete(CacheKeyMapper.routine_api_logs(instance.routine.id, instance.user_id))
             reset_routine_cache(instance.routine, structure=False)
     
     
    
  • wger/utils/cache.py+12 12 modified
    @@ -82,28 +82,28 @@ def get_exercise_api_key(cls, base_uuid: str):
             return f'base-uuid-{base_uuid}'
     
         @classmethod
    -    def routine_date_sequence_key(cls, id: int):
    -        return f'routine-date-sequence-{id}'
    +    def routine_date_sequence_key(cls, pk: int):
    +        return f'routine-date-sequence-{pk}'
     
         @classmethod
    -    def routine_api_date_sequence_display_key(cls, pk: int):
    -        return f'routine-api-date-sequence-display-{pk}'
    +    def routine_api_date_sequence_display_key(cls, pk: int, user_id: int):
    +        return f'routine-api-date-sequence-display-{user_id}-{pk}'
     
         @classmethod
    -    def routine_api_date_sequence_gym_key(cls, pk: int):
    -        return f'routine-api-date-sequence-gym-{pk}'
    +    def routine_api_date_sequence_gym_key(cls, pk: int, user_id: int):
    +        return f'routine-api-date-sequence-gym-{user_id}-{pk}'
     
         @classmethod
    -    def routine_api_stats(cls, pk: int):
    -        return f'routine-api-stats-{pk}'
    +    def routine_api_stats(cls, pk: int, user_id: int):
    +        return f'routine-api-stats-{user_id}-{pk}'
     
         @classmethod
    -    def routine_api_logs(cls, pk: int):
    -        return f'routine-api-logs-{pk}'
    +    def routine_api_logs(cls, pk: int, user_id: int):
    +        return f'routine-api-logs-{user_id}-{pk}'
     
         @classmethod
    -    def routine_api_structure_key(cls, pk: int):
    -        return f'routine-api-structure-{pk}'
    +    def routine_api_structure_key(cls, pk: int, user_id: int = None):
    +        return f'routine-api-structure-{user_id}-{pk}'
     
         @classmethod
         def slot_entry_configs_key(cls, pk: int):
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.