Forums Bug Reports Thread

cmi5 launch does not verify learner enrollment/entitlement before minting a session

Patrick Bass · Jun 6 · 9 · 1 Locked
[Minor] [Normal Priority] [Bug Fixed] [Always Reproduces]
🚀 OP Jun 6, 2026 6:45pm

Area: mobieusLearn (audit p8) · Surface: POST /learn/cmi5/launch/{auId}/start · Dimension: security · Severity: minor

Any authenticated learner can launch any cmi5 AU belonging to a 'ready' course by iterating auId, regardless of whether they purchased/enrolled. The launch session is scoped to their own learner_id so it is not cross-user data theft, but it bypasses the purchase entitlement that sold courses rely on (mobieusCore course-sales), letting a user consume paid cmi5 content without enrolling. Lower severity because it grants content access, not data exposure, and only on tenants using cmi5 packages.

Evidence

platform/src/Controllers/Learn/LearnCmi5RuntimeController.php:42-88 start(): after requireAuth()+validateCsrf(), it loads the AU via `Cmi5Au::findById((int)$auId)` and the course via `Cmi5Course::findById`, checks only `($course['status'] ?? '') !== 'ready'`, resolves the learner, then immediately creates a session and composes a launch URL — with no check that this learner is enrolled in or entitled to the course. Compare LearnScormRuntimeController/ScormRuntimeService::findOrStartAttempt which enforces enrollment ownership (ScormRuntimeService.php:81 `if ($enr === null || (int)$enr['user_id'] !== (int)$userId) throw ...`), and PlayerController::resolveAccess (PlayerController.php:386-404) which enforces hasActiveEntitlement for sold courses.

Suggested fix. In LearnCmi5RuntimeController::start(), after resolving the learner, resolve the owning course's learn_course and require an active enrollment/entitlement (mirror PlayerController::resolveAccess: if the course is sold, require EnrollmentService::hasActiveEntitlement; otherwise EnrollmentService::ensureSelf). Return 403 when the learner has no entitlement before creating the Cmi5Session row.

Filed by the automated tenant-app audit and adversarially evidence-verified. Status: verified. Open — not yet actioned.


Patrick Bass
@mobieus

🚀 Jun 7, 2026 5:44am

Resolved — fixed and deployed. Commit 059d6bf29b6f, shipped dev-first then to all tenants on 2026-06-06.

Added an entitlement gate to LearnCmi5RuntimeController::start() mirroring PlayerController::resolveAccess. After resolving the learner it resolves the owning native learn_courses id (activity.cmi5_au_id -> lesson -> module -> course), fails closed 403 if the AU is not wired into any course, requires EnrollmentService::hasActiveEntitlement when the course is sold (else redirects to /learn/courses), and auto-self-enrolls via EnrollmentService::ensureSelf for free/private courses. Added helpers owningLearnCourseId/learnCourseIsSold/learnContext and imports for EnrollmentService + LearnContext. php -l clean.

Status: fixed. Thread closed and locked.


Patrick Bass
@mobieus

Log in or register to reply to this thread.