Skip to content

[EPIC] Security: broken access control (BOLA/IDOR) remediation #59

Description

@franciszver

Summary

A whole-repo security review (2026-07-24) found one systemic vulnerability class: broken access control (BOLA/IDOR). API endpoints authenticate the caller — or skip auth entirely — but never verify that the requested object belongs to that caller. Passing another user's ID in the path, query, or body returns or mutates their data.

Impact is amplified because tutor and parent roles are self-registerable via the public /auth/register (_ALLOWED_REGISTER_ROLES = {"student","tutor","parent"}), so the role gates guarding several endpoints are not a real trust boundary.

Clean dimensions (no findings): SQL/command injection, unsafe deserialization, path traversal, SSRF, frontend XSS, committed secrets, JWT crypto (HS256 pinned, no default secret), CORS (prod-pinned), CI workflows.

Review method: Fable orchestrated 5 parallel finder subagents by dimension → independent per-finding verification subagents → kept confidence ≥ 8 as confirmed.

Confirmed findings (confidence ≥ 8)

# Endpoint(s) File Problem Severity
1 GET /api/v1/summaries/{user_id} src/api/handlers/summaries.py:104 authenticated, no ownership check High
2 POST /practice/assign /complete /summary src/api/handlers/practice.py:112,426,539 unauthenticated (optional auth, result unused) High
3 GET /nudges/users/{user_id} POST /nudges/check /{id}/engage src/api/handlers/nudges.py:37,126,298 unauthenticated High
4 GET /enhancements/qa/conversation-context/{student_id} src/api/handlers/enhancements.py:82 self-check present on sibling route, omitted here High
5 GET /messaging/threads?user_id= src/api/handlers/messaging.py:224 caller-supplied user_id overrides JWT identity High
6 GET /dashboards/parent/student/{id} /parent/students src/api/handlers/dashboards.py:29 self-registerable parent role, no relationship check, returns all students High
7 POST /overrides/ src/api/handlers/overrides.py:36 trusts body tutor_id/student_id/target_id → forge audit trail + mutate content High
8 GET /overrides/{student_id} src/api/handlers/overrides.py:147 no tutor↔student relationship check Medium

Related — confirmed real, lower confidence (fix in same sweep)

  • 9. jobs.py GET /jobs/{job_id} + WS /jobs/{job_id}/ws — returns/streams another student's generated practice result; WS has no auth dependency at all. Bounded by unguessable job UUIDv4 (leak-via-share, not enumeration).
  • 10. advanced_analytics.py GET /analytics/advanced/engagement/{user_id} — any tutor/parent reads any student's 30-day activity profile. No direct PII, so bounded.

Root causes

  1. No shared object-level authorization primitive — handlers do authentication + coarse role, but nothing asserts "caller owns/relates to this object." Correct examples already exist (goals.py, progress.py, messaging.get_thread, enhancements.get_conversation_history).
  2. get_current_user_optional used where auth should be mandatory (practice, nudges, jobs), then never read.
  3. Identity trusted from request body/query instead of the JWT (overrides.tutor_id, messaging ?user_id).
  4. TutorStudentAssignment (src/models/tutor_student.py) exists but is never queried; no parent↔student model exists at all.
  5. Over-permissive self-registration of tutor/parent.

Remediation phases

Order: #60#61 → (#62, #63, #64 in parallel) → #65. Phase 1 (#61) is the shared foundation; Phases 2–4 can run in parallel across sessions once it lands.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions