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