Forums Bug Reports Thread

Missing CSRF protection on all SCORM runtime state-changing POSTs

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

Area: mobieusLearn (audit p8) · Surface: POST /learn/scorm/launch/{eid}/{aid}/start, /learn/scorm/launch/{attemptId}/next, /prev, /learn/api/scorm/{attemptId}/set-value, /commit, /terminate · Dimension: security · Severity: minor

Every other state-changing Learn POST (cohorts, paths, live-sessions, AI endpoints) calls $this->validateCsrf() inline because the public group provides no CSRF middleware, but the SCORM runtime controller omits it entirely. Exploitability is limited: the API handlers read the body from php://input as JSON (json() at line 259), so a classic auto-submitting HTML form (application/x-www-form-urlencoded) decodes to [] and the element is empty; and ScormRuntimeService::assertAttemptOwner (ScormRuntimeService.php:335-340) blocks cross-user writes. The realistic CSRF impact is forcing a victim's own attempt to be marked complete/terminated via the next/prev/terminate endpoints. Still a genuine missing-control gap versus the documented convention (CLAUDE.md: 'call validateCsrf() at start of every POST handler').

Evidence

These routes are registered in the public group at platform/src/routes.php:340 whose middleware list contains NO CsrfValidationMiddleware and NO AuthMiddleware (confirmed: PerformanceMiddleware, ProbeDetectionMiddleware, IpBanMiddleware, LlmBotBlockMiddleware, MaintenanceMiddleware, AccessModeMiddleware, LurkerTrackerMiddleware, SuspensionGateMiddleware, GateMiddleware). The handlers in platform/src/Controllers/Learn/LearnScormRuntimeController.php contain zero validateCsrf() calls (`grep -c validateCsrf` = 0). `start()` (line 34) only calls `$this->requireLearner()`; the API handlers (apiSetValue line 188, apiCommit 203, apiTerminate 210) call only `$this->jsonOnly()` which just sets a Content-Type header. These mutate state: start() find-or-creates an attempt; next/prev mark the attempt complete (line 82-85 `UPDATE learn_attempts SET status="complete"...`); set-value/commit/terminate write CMI data and finalize attempts.

Suggested fix. Add $this->validateCsrf() (or move these routes into a group that includes CsrfValidationMiddleware) to start(), nextSco(), prevSco(), apiSetValue(), apiCommit(), apiTerminate(). For the JSON API endpoints the front-end already sends X-CSRF-TOKEN via the app.min.js fetch wrapper, so adding the check is non-breaking; the SCO iframe submitting cross-origin would need the token threaded through, matching how cmi5 start() already does requireAuth()+validateCsrf().

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 $this->validateCsrf() to all six state-mutating SCORM runtime POST handlers: start(), nextSco(), prevSco() (after requireLearner()), and apiSetValue(), apiCommit(), apiTerminate() (after jsonOnly(), before reading the body/touching state). The JSON API endpoints already receive X-CSRF-TOKEN via the app.min.js fetch wrapper, so the check passes in normal use. Left read-only apiGetValue() unchanged per the audit scope; the internal navigate()->start() path re-validates harmlessly since the token persists within the same request. php -l passes.

Status: fixed. Thread closed and locked.


Patrick Bass
@mobieus

Log in or register to reply to this thread.