Files
WerkoutAPI/hardening-report.md
Trey t c80c66c2e5 Codebase hardening: 102 fixes across 35+ files
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>
2026-02-27 22:29:14 -06:00

483 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.