Deep audit identified 106 findings; 102 fixed, 4 deferred. Covers 8 areas: - Settings & deploy: env-gated DEBUG/SECRET_KEY, HTTPS headers, gunicorn, celery worker - Auth (registered_user): password write_only, request.data fixes, transaction safety, proper HTTP status codes - Workout app: IDOR protection, get_object_or_404, prefetch_related N+1 fixes, transaction.atomic - Video/scripts: path traversal sanitization, HLS trigger guard, auth on cache wipe - Models (exercise/equipment/muscle/superset): null-safe __str__, stable IDs, prefetch support - Generator views: helper for registered_user lookup, logger.exception, bulk_update, transaction wrapping - Generator core (rules/selector/generator): push-pull ratio, type affinity normalization, modality checks, side-pair exact match, word-boundary regex, equipment cache clearing - Generator services (plan_builder/analyzer/normalizer): transaction.atomic, muscle cache, bulk_update, glutes classification fix Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
483 lines
26 KiB
Markdown
483 lines
26 KiB
Markdown
# Hardening Audit Report — Werkout API (Django/Python)
|
||
|
||
## Audit Sources
|
||
- 5 mapper agents (100% file coverage)
|
||
- 8 specialized domain auditors (parallel)
|
||
- 1 cross-cutting deep audit (parallel)
|
||
- Total source files: 75
|
||
|
||
---
|
||
|
||
## CRITICAL — Will crash or lose data (18 findings)
|
||
|
||
**1. werkout_api/settings.py:16** | DEBUG=True hardcoded, never disabled in production
|
||
- What: `DEBUG = True` set at module level. Production branch (when `DATABASE_URL` set) never overrides to `False` — the code is commented out (lines 142-157). `CORS_ALLOW_ALL_ORIGINS` on line 226 depends on DEBUG, so it's always `True`.
|
||
- Impact: Full stack traces, SQL queries, internal paths exposed to end users. CORS allows any origin with credentials.
|
||
- Source: Security, Config, Cross-cutting
|
||
|
||
**2. werkout_api/settings.py:160** | SECRET_KEY falls back to 'secret'
|
||
- What: `SECRET_KEY = os.environ.get("SECRET_KEY", 'secret')`. Neither `docker-compose.yml` nor any env file sets SECRET_KEY.
|
||
- Impact: Session cookies, CSRF tokens, password hashes use a publicly known key. Complete auth bypass.
|
||
- Source: Security, Config
|
||
|
||
**3. werkout_api/settings.py:226** | CORS allows all origins with credentials in production
|
||
- What: `CORS_ALLOW_ALL_ORIGINS = True if DEBUG else False` is always `True` (DEBUG never False). Combined with `CORS_ALLOW_CREDENTIALS = True` (line 231).
|
||
- Impact: Any website can make authenticated cross-origin requests and steal data.
|
||
- Source: Security, Config
|
||
|
||
**4. registered_user/serializers.py:31** | Password hash exposed in API responses
|
||
- What: `write_only_fields = ('password',)` is NOT a valid DRF Meta option. Correct: `extra_kwargs = {'password': {'write_only': True}}`. Password field is readable.
|
||
- Impact: Hashed password returned in registration responses. Enables offline brute-force.
|
||
- Source: Security, API, Cross-cutting
|
||
|
||
**5. registered_user/views.py:83-90** | update_registered_user uses request.POST — JSON requests wipe user data
|
||
- What: `request.POST.get(...)` only works for form-encoded data. JSON requests return `None` for all fields. Lines 88-90 set `first_name=None`, `email=None`, etc. and save.
|
||
- Impact: Any JSON profile update silently corrupts user data. Email set to None breaks login.
|
||
- Source: Security, Logic, Cross-cutting
|
||
|
||
**6. registered_user/views.py:108-114** | Password update broken for JSON clients, can lock out user
|
||
- What: `request.POST.get("new_password")` returns `None` for JSON. `set_password(None)` makes password uncheckable, permanently locking user out.
|
||
- Impact: Password endpoint non-functional for JSON clients. Potential permanent account lockout.
|
||
- Source: Security, Cross-cutting
|
||
|
||
**7. registered_user/serializers.py:46** | Registration creates RegisteredUser with non-existent phone_number field
|
||
- What: `RegisteredUser.objects.create(phone_number=self.context.get("phone_number"), ...)` — model has no `phone_number` field (removed in migration 0002).
|
||
- Impact: User registration crashes with TypeError if phone_number is passed in context.
|
||
- Source: Cross-cutting, Logic
|
||
|
||
**8. scripts/views.py:43-45** | Anonymous cache wipe endpoint — no authentication
|
||
- What: `clear_redis` view has no auth decorators. Active in `scripts/urls.py`. Any anonymous request wipes entire Redis cache.
|
||
- Impact: Denial of service — any internet user can flush all cached data at will.
|
||
- Source: Security
|
||
|
||
**9. video/views.py:50-59** | Path traversal vulnerability in hls_videos
|
||
- What: `video_name` and `video_type` from GET params concatenated directly into file paths without sanitization. `../../etc/passwd` sequences can access arbitrary files.
|
||
- Impact: Arbitrary file read on the server. Route commented out in urls.py but view exists.
|
||
- Source: Security
|
||
|
||
**10. video/views.py:74** | Celery task called with zero arguments but requires filename
|
||
- What: `create_hls_tasks.delay()` called with no args. Task signature `create_hls_tasks(filename)` requires one.
|
||
- Impact: Every call to `/videos/create_hls/` crashes the Celery worker with TypeError.
|
||
- Source: Celery, Cross-cutting
|
||
|
||
**11. supervisord.conf:13** | Production runs Django dev server (runserver) instead of WSGI
|
||
- What: `python manage.py runserver 0.0.0.0:8000` in production. `uwsgi.ini` exists but is unused.
|
||
- Impact: Single-threaded, no request timeouts, not designed for production. Memory leaks.
|
||
- Source: Config
|
||
|
||
**12. supervisord.conf** | No Celery worker process configured
|
||
- What: Only `django` and `nextjs` programs defined. No `[program:celery]` entry.
|
||
- Impact: All `.delay()` calls queue tasks in Redis that are never consumed. Entire async task system non-functional.
|
||
- Source: Celery, Config
|
||
|
||
**13. supervisord.conf:13** | Auto-migrate on every container start
|
||
- What: `python manage.py migrate` in startup command runs migrations automatically without review.
|
||
- Impact: Destructive migrations run silently. Race conditions if multiple containers start simultaneously.
|
||
- Source: Config
|
||
|
||
**14. docker-compose.yml:8-10,26** | Database credentials hardcoded as postgres/postgres
|
||
- What: `POSTGRES_USER=postgres`, `POSTGRES_PASSWORD=postgres` in compose file and DATABASE_URL. No `.env` override.
|
||
- Impact: Trivial unauthorized access if database port exposed. Credentials in git history permanently.
|
||
- Source: Security, Config
|
||
|
||
**15. AI/workouts.py + AI/cho/workouts.py** | 86K lines of PII data committed to git
|
||
- What: Two files totaling 86,000+ lines of user workout data from Future Fitness API with user IDs, S3 URLs, timestamps.
|
||
- Impact: PII permanently in git history. Potential GDPR/privacy liability.
|
||
- Source: Security, Config
|
||
|
||
**16. generator/views.py:1032-1160** | save_plan has no transaction wrapping
|
||
- What: Creates GeneratedWeeklyPlan, then loops creating Workout, Superset, SupersetExercise, GeneratedWorkout, PlannedWorkout objects. No `transaction.atomic()`.
|
||
- Impact: Mid-loop failure (e.g., date parsing) leaves orphaned plan records. Partially saved plans with missing days.
|
||
- Source: Data Integrity, Cross-cutting
|
||
|
||
**17. generator/views.py:789-803** | confirm_plan has no transaction wrapping
|
||
- What: Loops through generated workouts, saves status, deletes/creates PlannedWorkouts individually.
|
||
- Impact: Partial plan confirmation — some days accepted, others not, on any mid-loop error.
|
||
- Source: Data Integrity
|
||
|
||
**18. registered_user/serializers.py:34-51** | User + RegisteredUser creation has no transaction
|
||
- What: `User.objects.create()`, `set_password()`, `RegisteredUser.objects.create()`, `Token.objects.create()` — four DB ops with no `transaction.atomic()`.
|
||
- Impact: Orphaned User records if RegisteredUser creation fails. Ghost users block re-registration.
|
||
- Source: Data Integrity, Cross-cutting
|
||
|
||
---
|
||
|
||
## BUG — Incorrect behavior (28 findings)
|
||
|
||
**1. registered_user/views.py:30,47** | Validation errors return HTTP 500 instead of 400
|
||
- Impact: Clients can't distinguish server errors from bad input.
|
||
|
||
**2. registered_user/views.py:74** | Failed login returns 404 instead of 401
|
||
- Impact: Wrong HTTP semantics for auth failures.
|
||
|
||
**3. registered_user/models.py:20** | `__str__` concatenates nullable last_name — TypeError
|
||
- Impact: Admin, logging crash for users with null last_name.
|
||
|
||
**4. registered_user/admin.py:11** | Token.objects.get crashes if no token exists
|
||
- Impact: Admin list page crashes if any user lacks a Token.
|
||
|
||
**5. equipment/models.py:13** | `__str__` concatenates nullable category/name — TypeError
|
||
- Impact: Admin crashes for Equipment with null fields.
|
||
|
||
**6. muscle/models.py:11** | `__str__` returns None when name is null
|
||
- Impact: Violates `__str__` contract. Admin/template crashes.
|
||
|
||
**7. workout/views.py:45** | Workout.objects.get with no DoesNotExist handling
|
||
- Impact: Missing workouts return 500 instead of 404.
|
||
|
||
**8. workout/views.py:60,143,165** | Validation errors return HTTP 500 instead of 400
|
||
- Impact: Three views misreport client errors as server errors.
|
||
|
||
**9. workout/views.py:69** | GET endpoint returns 201 Created instead of 200
|
||
- Impact: Incorrect HTTP semantics for read operation.
|
||
|
||
**10. workout/views.py:76** | Unreachable None check — .get() raises exception, never returns None
|
||
- Impact: Dead code; actual DoesNotExist is unhandled (500 error).
|
||
|
||
**11. workout/views.py:124** | estimated_rep_duration None multiplication crashes
|
||
- What: `exercise["reps"] * exercise_obj.estimated_rep_duration` where field can be null. `int * None` = TypeError.
|
||
- Impact: Workout creation crashes mid-loop, orphaning partial records (no transaction).
|
||
|
||
**12. workout/serializers.py:37** | KeyError if 'notes' not in validated_data
|
||
- Impact: Completing a workout without notes crashes with 500.
|
||
|
||
**13. workout/serializers.py:40** | Wrong attribute name — health_kit UUID never persisted
|
||
- What: Sets `completed_workout.workout_uuid` but model field is `health_kit_workout_uuid`.
|
||
- Impact: HealthKit UUIDs silently discarded forever.
|
||
|
||
**14. workout/tasks.py:85** | estimated_rep_duration None multiplication in Celery task
|
||
- Impact: Bulk import crashes mid-way, leaving partial data.
|
||
|
||
**15. workout/tasks.py:73** | Exercise.objects.get with no DoesNotExist handling
|
||
- Impact: One missing exercise aborts entire import.
|
||
|
||
**16. workout/urls.py:14** | Duplicate URL name 'plan workout' on two paths
|
||
- Impact: `reverse('plan workout')` resolves to wrong URL.
|
||
|
||
**17. scripts/views.py:37** | NameError: MuscleGroup is not defined
|
||
- What: Catches `MuscleGroup.DoesNotExist` but only `Muscle` is imported.
|
||
- Impact: NameError crashes endpoint instead of catching intended exception.
|
||
|
||
**18. scripts/views.py:15** | equipment_required.split() crashes on None
|
||
- Impact: sync_equipment crashes for any exercise with null equipment_required.
|
||
|
||
**19. video/models.py:24** | save() missing *args in signature
|
||
- Impact: Callers passing positional args (force_insert) get TypeError.
|
||
|
||
**20. video/models.py:24-27** | HLS transcoding triggered on EVERY save, not just file changes
|
||
- Impact: Redundant expensive ffmpeg jobs on metadata-only edits.
|
||
|
||
**21. video/serializers.py:13** | video_file can be None — AttributeError
|
||
- Impact: Video listing crashes if any Video has no file.
|
||
|
||
**22. video/tasks.py:10** | Existence check uses wrong filename pattern — never matches
|
||
- Impact: Guard clause never short-circuits; re-encodes every time.
|
||
|
||
**23. generator/views.py:70** | RegisteredUser.objects.get repeated ~17 times with no DoesNotExist handling
|
||
- Impact: Any user without RegisteredUser gets unhandled 500 on every generator endpoint.
|
||
|
||
**24. superset/helpers.py:16** | Exercise.objects.get("First Up") with no error handling
|
||
- Impact: Workout detail crashes if "First Up" exercise is missing.
|
||
|
||
**25. superset/serializers.py:20** | get_unique_id returns random UUID per serialization
|
||
- Impact: Frontend can't use unique_id as stable key. Breaks diffing/caching.
|
||
|
||
**26. workout/models.py:51** | settings not imported — NameError on duration_audio()/weight_audio()
|
||
- What: Relies on `from exercise.models import *` transitive import of settings.
|
||
- Impact: NameError if transitive chain breaks.
|
||
|
||
**27. workout_generator.py:909** | None multiplication when duration is None
|
||
- Impact: Plan generation crashes if preferences have no duration set.
|
||
|
||
**28. workout_generator.py:802** | sum(c.difficulty) crashes if any difficulty is None
|
||
- Impact: Plan generation crashes for users with incomplete completion records.
|
||
|
||
---
|
||
|
||
## SILENT FAILURE — Error swallowed or ignored (5 findings)
|
||
|
||
**1. generator/views.py:193,491,874,989,1156** | Broad except Exception catches all errors, leaks str(e)
|
||
- Impact: Bugs masked. Internal details leaked to clients.
|
||
|
||
**2. superset/helpers.py:19-23** | In-memory mutations on Exercise ORM object never saved
|
||
- Impact: Changes silently lost. Risk of corrupting shared Exercise if accidentally saved.
|
||
|
||
**3. workout/helpers.py:41** | ser_data.mutable = True is a no-op
|
||
- Impact: No effect. Indicates confusion about data type.
|
||
|
||
**4. audit_exercise_data.py:168-170** | except Exception: pass silently swallows all errors
|
||
- Impact: Database errors during field checks silently ignored.
|
||
|
||
**5. workout/views.py:32** | Infinite cache with incomplete invalidation
|
||
- Impact: Generated workouts never appear in all_workouts until manual cache clear.
|
||
|
||
---
|
||
|
||
## RACE CONDITION — Concurrency issue (1 finding)
|
||
|
||
**1. registered_user/views.py:34** | Email uniqueness check is a race condition
|
||
- What: `User.objects.filter(email=email)` check followed by `serializer.save()`. No DB unique constraint visible.
|
||
- Impact: Concurrent registrations can create duplicate email accounts.
|
||
|
||
---
|
||
|
||
## LOGIC ERROR — Code doesn't match intent (12 findings)
|
||
|
||
**1. rules_engine.py:650** | Push/pull ratio check skipped when either count is zero
|
||
- What: Condition requires both counts > 0. A workout with 2 push, 0 pull passes silently.
|
||
- Impact: Unbalanced push-heavy workouts pass validation.
|
||
|
||
**2. rules_engine.py:858-860** | Workout type match is a no-op for non-strength types
|
||
- What: Non-strength branch unconditionally counts every exercise as matching (100% always).
|
||
- Impact: HIIT/cardio/core workouts can contain arbitrary exercises without violations.
|
||
|
||
**3. workout_generator.py:1459** | Workout type affinity matching NEVER works
|
||
- What: `SPLIT_TYPE_WORKOUT_AFFINITY` uses underscore names like `'traditional_strength_training'` but comparison uses `wt.name.strip().lower()` which yields space-separated names.
|
||
- Impact: All workout type assignments fall through to round-robin fallback. Push splits get assigned random types.
|
||
|
||
**4. workout_generator.py:2070** | Modality check counts exercise capability, not actual assignment
|
||
- What: Checks `ex.is_duration` (capability flag) not whether the entry was actually given duration.
|
||
- Impact: False modality calculations for dual-modality exercises.
|
||
|
||
**5. workout_generator.py:1404** | Diversify type count wrong on replacement
|
||
- What: Doesn't subtract from the removed type count when replacing, only adds to candidate count.
|
||
- Impact: Valid replacements rejected. Invalid ones accepted in edge cases.
|
||
|
||
**6. workout_generator.py:2898** | Final conformance treats all warnings as blocking
|
||
- What: `_is_blocking_final_violation` returns True for `severity in {'error', 'warning'}`.
|
||
- Impact: Workouts crash with ValueError for minor advisory issues (cooldown missing, duration bias slightly off).
|
||
|
||
**7. workout_generator.py:1209** | Recursive retry destroys cross-day dedup state
|
||
- What: Failed attempt's exercises already recorded in week state via `accumulate_week_state`. Retry with different exercises creates ghost entries.
|
||
- Impact: Later days in the week have artificially smaller exercise pools.
|
||
|
||
**8. entry_rules.py:19** | Volume floor can violate workout type rep ranges
|
||
- What: With `min_volume=12` and `rounds=1`, forces 12 reps. Strength (3-6 rep range) gets 12 reps.
|
||
- Impact: Strength workouts get inflated reps contradicting their character.
|
||
|
||
**9. rules_engine.py:441** | Push/pull counting double-counts dual-pattern exercises
|
||
- What: Exercise with `'upper push, upper pull'` counted in BOTH push AND pull totals.
|
||
- Impact: Inaccurate push:pull ratio calculations.
|
||
|
||
**10. exercise_selector.py:631** | No-equipment path restricts to bodyweight only (contradicts docs)
|
||
- What: MEMORY.md says "no equipment set = all exercises available." Code excludes all exercises with equipment entries.
|
||
- Impact: Users without equipment config get dramatically reduced pool.
|
||
|
||
**11. muscle_normalizer.py:163** | Glutes in both lower_push and lower_pull categories
|
||
- Impact: Glute-dominant workouts get incorrect split classification, cascading into wrong type assignments.
|
||
|
||
**12. exercise_selector.py:1274** | Substring partner matching causes false positives
|
||
- What: `if base_name.lower() in partner.name.lower()` — "Curl" matches "Barbell Curl Right", "Hammer Curl Right", etc.
|
||
- Impact: Wrong exercises paired as L/R counterparts.
|
||
|
||
---
|
||
|
||
## PERFORMANCE — Unnecessary cost (18 findings)
|
||
|
||
**1. exercise/serializers.py:30,35** | N+1 per exercise for muscles + equipment (~3400+ queries on cache miss)
|
||
- Impact: `/exercise/all/` cold cache: 1133 exercises × 3 queries each.
|
||
|
||
**2. workout/serializers.py:56-77** | Triple N+1 on WorkoutSerializer (~5000+ queries)
|
||
- Impact: `all_workouts` cache miss: 633 workouts × (muscles + equipment + exercise_count).
|
||
|
||
**3. superset/serializers.py:32** | N+1 per superset for exercises, cascading through ExerciseSerializer
|
||
- Impact: Each workout detail triggers O(supersets × exercises × 3) queries.
|
||
|
||
**4. workout/helpers.py:14-71** | Cascade of N+1 queries in exercise list builder
|
||
- Impact: ~80+ queries per workout detail (supersets + exercises + serializer chain).
|
||
|
||
**5. generator/serializers.py:338** | N+1 for supersets in GeneratedWorkoutDetailSerializer
|
||
- Impact: Plan detail views trigger dozens of cascading queries per day.
|
||
|
||
**6. generator/views.py:1106** | Exercise.objects.get in triple-nested loop in save_plan
|
||
- Impact: 5-day plan with 5 supersets × 3 exercises = 75 individual SELECT queries.
|
||
|
||
**7. muscle_normalizer.py:218** | ExerciseMuscle query per exercise in analyzer (~19,000 queries)
|
||
- Impact: `analyze_workouts` command fires ~19,000 queries for 633 workouts.
|
||
|
||
**8. workout_analyzer.py:1332-1337** | 120 exists() checks in _step7
|
||
- Impact: 8 types × 3 sections × 5 goals = 120 individual queries.
|
||
|
||
**9. recalculate_workout_times.py:53-58** | Triple-nested N+1 with no prefetch (~18,000 queries)
|
||
- Impact: Command takes orders of magnitude longer than necessary.
|
||
|
||
**10. exercise_selector.py:593,629** | M2M querysets not cached (excluded_exercises + available_equipment)
|
||
- Impact: 15-30 redundant identical queries per workout generation.
|
||
|
||
**11. populate_exercise_fields.py:1006** | Individual save() per exercise (1133 UPDATE queries)
|
||
- Impact: Command takes minutes instead of seconds. No bulk_update.
|
||
|
||
**12. plan_builder.py:64,82** | Redundant save() after create() on Workout and Superset
|
||
- Impact: 2 unnecessary DB writes per superset creation.
|
||
|
||
**13. Various views** | Infinite cache with no invalidation strategy
|
||
- What: equipment, exercise, muscle, workout, video views all use `cache.set(key, data, timeout=None)` with no invalidation.
|
||
- Impact: New/edited data never appears until manual cache clear or restart.
|
||
|
||
**14. workout/serializers.py:109** | Redundant re-fetch of registered_user
|
||
- Impact: Extra query per workout detail for no reason.
|
||
|
||
**15. generator/views.py:570-572,604-607** | N+1 save pattern for re-ordering after delete
|
||
- Impact: Up to N individual UPDATEs instead of 1 bulk_update.
|
||
|
||
**16. generator/views.py:423-429,964-976** | N+1 for sibling exercise exclusion
|
||
- Impact: N queries instead of 1 IN query for sibling workout exercises.
|
||
|
||
**17. generator/views.py:70** | RegisteredUser.objects.get repeated 17x with no caching
|
||
- Impact: 1 unnecessary query per API request across all generator endpoints.
|
||
|
||
**18. exercise_selector.py:1063** | Potentially large retry loop in _weighted_pick
|
||
- What: `max_attempts = len(pool) * 3` with weighted pools of 500+ entries = 1500+ iterations.
|
||
- Impact: CPU-bound stall risk in constrained pools.
|
||
|
||
---
|
||
|
||
## SECURITY — Vulnerability or exposure (6 additional findings)
|
||
|
||
**1. werkout_api/settings.py:140** | ALLOWED_HOSTS=['*'] in production
|
||
- Impact: HTTP Host header injection, cache poisoning, password reset URL manipulation.
|
||
|
||
**2. werkout_api/settings.py:1-231** | Missing all HTTPS/security hardening settings
|
||
- What: No SECURE_SSL_REDIRECT, SECURE_HSTS_SECONDS, SESSION_COOKIE_SECURE, CSRF_COOKIE_SECURE, etc.
|
||
- Impact: Cookies sent over plaintext HTTP. No HSTS protection.
|
||
|
||
**3. werkout_api/settings.py:31** | Django Debug Toolbar enabled unconditionally
|
||
- Impact: Exposes SQL queries, settings, request data at `/__debug__/` in production.
|
||
|
||
**4. workout/views.py:24-33** | all_workouts returns ALL users' workouts (IDOR)
|
||
- What: `Workout.objects.all()` with no ownership filter.
|
||
- Impact: Any authenticated user sees every user's workout data.
|
||
|
||
**5. workout/views.py:39-49** | workout_details has no ownership check (IDOR)
|
||
- What: Any authenticated user can view any workout by guessing IDs.
|
||
- Impact: Insecure Direct Object Reference.
|
||
|
||
**6. workout/views.py:170-172** | GET endpoint triggers data mutation — bulk import
|
||
- What: GET triggers Celery task importing workouts for hardcoded user IDs. Any authenticated user can trigger.
|
||
- Impact: Data corruption via idempotent-violating GET.
|
||
|
||
---
|
||
|
||
## DATA INTEGRITY — Database/model consistency issues (5 findings)
|
||
|
||
**1. workout/views.py:94-138** | add_workout has no transaction wrapping
|
||
- Impact: Partial Workout/Superset records on mid-loop failure.
|
||
|
||
**2. plan_builder.py:59-149** | create_workout_from_spec has no transaction wrapping
|
||
- Impact: Core builder used by all generation paths creates orphaned records on error.
|
||
|
||
**3. workout_analyzer.py:249-252** | _clear_existing_patterns deletes without transaction
|
||
- Impact: If analysis crashes mid-way, ML pattern tables are empty with no recovery.
|
||
|
||
**4. workout/tasks.py:11-101** | Bulk import has no transaction or idempotency
|
||
- Impact: Partial imports, duplicate records on re-run.
|
||
|
||
**5. workout/views.py:150** | datetime.now() without timezone in USE_TZ=True project
|
||
- Impact: Incorrect PlannedWorkout filtering near midnight due to timezone mismatch.
|
||
|
||
---
|
||
|
||
## MODERNIZATION — Legacy pattern to update (4 findings)
|
||
|
||
**1. Dockerfile:13** | Python 3.9.13 base image (EOL October 2025)
|
||
- Impact: No further security patches.
|
||
|
||
**2. requirements.txt** | All dependencies pinned to mid-2023 versions
|
||
- Impact: Django 4.2.2 has had multiple security releases since.
|
||
|
||
**3. supervisord.conf:24** | Next.js runs `next dev` in production
|
||
- Impact: No production optimizations, source maps exposed.
|
||
|
||
**4. Various models** | max_length on IntegerField/FloatField (no-op parameters)
|
||
- What: 10+ fields across superset, workout, exercise models use meaningless `max_length` on numeric fields.
|
||
- Impact: Misleading — suggests validation that doesn't exist.
|
||
|
||
---
|
||
|
||
## DEAD CODE / UNREACHABLE (4 findings)
|
||
|
||
**1. exercise/serializers.py:5** | Import shadowed by local class definition
|
||
- What: Imports `ExerciseMuscleSerializer` then redefines it locally.
|
||
|
||
**2. exercise/models.py:4** | `from random import randrange` — imported but never used
|
||
|
||
**3. audit_exercise_data.py:88-89** | Dead `.exclude()` clause — logically impossible condition
|
||
|
||
**4. workout/views.py:76** | Unreachable None check after `.get()`
|
||
|
||
---
|
||
|
||
## FRAGILE — Works now but will break easily (5 findings)
|
||
|
||
**1. exercise_selector.py:613** | Hard exclude to soft penalty conversion mutates instance state permanently
|
||
- What: `_warned_small_pool` guard uses `hasattr` which survives `reset()`.
|
||
- Impact: Once triggered, ALL subsequent selections treat hard-excluded exercises with soft penalty only.
|
||
|
||
**2. exercise_selector.py:645** | Equipment map cache survives reset() — stale data possible
|
||
- Impact: Low risk per-request but dangerous in long-running processes.
|
||
|
||
**3. workout_generator.py:1046** | Working superset detection relies on name prefix 'Working'
|
||
- Impact: Any naming inconsistency silently breaks trimming, padding, modality validation, compound ordering, rebalancing.
|
||
|
||
**4. workout/models.py:51** | settings import via wildcard chain from exercise.models
|
||
- Impact: Transitive dependency breaks if `*` re-export chain changes.
|
||
|
||
**5. exercise_selector.py:260** | Working set exclusion icontains 'stretch' catches valid exercises
|
||
- Impact: Exercises like "Stiff Leg Deadlift Stretch Position" incorrectly excluded from working sets.
|
||
|
||
---
|
||
|
||
## Summary
|
||
|
||
### Summary by Category
|
||
| Category | Count |
|
||
|----------|-------|
|
||
| Critical | 18 |
|
||
| Bug | 28 |
|
||
| Silent Failure | 5 |
|
||
| Race Condition | 1 |
|
||
| Logic Error | 12 |
|
||
| Performance | 18 |
|
||
| Security | 6 |
|
||
| Data Integrity | 5 |
|
||
| Modernization | 4 |
|
||
| Dead Code | 4 |
|
||
| Fragile | 5 |
|
||
| **Total** | **106** |
|
||
|
||
### Summary by Source
|
||
| Source | Findings |
|
||
|--------|----------|
|
||
| Security Auditor | 34 |
|
||
| Data Integrity/ORM Auditor | 64 |
|
||
| Logic Errors Auditor | 42 |
|
||
| Performance Auditor | 41 |
|
||
| Generator Logic Auditor | 22 |
|
||
| API Correctness Auditor | 43 |
|
||
| Celery/Async Auditor | 24 |
|
||
| Config/Deployment Auditor | 30 |
|
||
| Cross-cutting Deep Audit | 35 |
|
||
| *(after dedup)* | **106 unique** |
|
||
|
||
### Top 10 Priorities
|
||
|
||
1. **[CRITICAL] settings.py — DEBUG=True + SECRET_KEY='secret' + CORS wide open in production** — Three compounding security misconfigurations that enable session forgery, CSRF bypass, and full API data theft from any website.
|
||
|
||
2. **[CRITICAL] registered_user/views.py:83-90 — request.POST wipes user data on JSON update** — Any JSON profile update sets email, name, image all to None. Active, reachable endpoint.
|
||
|
||
3. **[CRITICAL] registered_user/serializers.py:31 — Password hash exposed in API** — Invalid DRF Meta option means hashed password is readable in registration responses.
|
||
|
||
4. **[CRITICAL] scripts/views.py:43 — Anonymous cache wipe** — Unauthenticated endpoint wipes entire Redis cache. Active route, no auth required.
|
||
|
||
5. **[CRITICAL] supervisord.conf — No Celery worker + dev server in production** — All async tasks (HLS transcoding, imports) silently queue and never execute. Django dev server handles all production traffic.
|
||
|
||
6. **[CRITICAL] generator/views.py — No transaction.atomic() on save_plan/confirm_plan** — Multi-object creation loops with no transaction wrapping leave orphaned records on any failure.
|
||
|
||
7. **[BUG] workout/serializers.py:40 — HealthKit UUID silently discarded** — Sets wrong attribute name (`workout_uuid` vs `health_kit_workout_uuid`). Data permanently lost.
|
||
|
||
8. **[BUG] workout/views.py:124 + tasks.py:85 — None multiplication on estimated_rep_duration** — Nullable field multiplied without null check. Crashes workout creation and bulk import.
|
||
|
||
9. **[LOGIC] workout_generator.py:1459 — Workout type affinity matching NEVER works** — Space vs underscore comparison means all type assignments fall through to random round-robin.
|
||
|
||
10. **[PERFORMANCE] Serializer N+1 queries — 5000+ queries on cache miss** — WorkoutSerializer, ExerciseSerializer, and SupersetSerializer each trigger per-object queries with no prefetching. Mitigated by infinite caching but devastating on any cache clear.
|