Area: Forums (re-run) (audit p2r) · Surface: POST /thread/{threadId}/move (ForumController@moveThread) · Dimension: Security (OWASP) · Severity: major
A forum moderator (forum-scoped, not site-wide) can move a thread out of a forum they moderate into a forum they have no authority over — including hidden/private forums, paid forums, or the mod-only escalations forum. This lets a single-forum mod inject content into restricted areas and remove content from public view by relocating it into a hidden forum. The privilege check covers only the source side; the target side is attacker-controlled via target_forum_id with no authorization. OWASP A01:2021 (Broken Access Control / IDOR).
Evidence
ForumController::moveThread (platform/src/Controllers/ForumController.php:5137-5168) only authorizes against the SOURCE forum: `if (!$this->canModerate((int) $user['id'], (int) $user['role'], (int) $thread['forum_id'])) { http_response_code(403); return; }`. It then loads the target purely by POST id: `$targetForumId = (int) ($_POST['target_forum_id'] ?? 0); $targetForum = Forum::findById($targetForumId);` and on success does `ForumThread::update((int) $thread['id'], ['forum_id' => $targetForumId]);` with NO check that the actor moderates, is a member of, or can even view the target forum. Contrast with crosspost (line 4300-4306) which explicitly re-checks the target: `if (!Forum::canPost($targetForumId, (int) $user['id'], (int) $user['role'])) { ... 'You cannot post in that forum.' }` — its own comment says 'Without this a source-forum moderator or thread author could shove a new thread into a private forum they have no access to.' moveThread is missing that exact guard.
Suggested fix. Mirror crosspost: after resolving $targetForum, require the actor to have authority over it — e.g. `if (!$this->canModerate((int)$user['id'],(int)$user['role'],$targetForumId)) { 403 }` (site admins role>=4 already pass canModerate). At minimum block moving into forums the actor cannot view/post in.
Filed by the automated tenant-app audit and adversarially evidence-verified. Status: verified. Open — not yet actioned.
Patrick Bass
@mobieus