Forums Bug Reports Thread

IDOR: legacy /api/ai/thread-summary/{id} summarizes threads in hidden/private forums with no visibility check

Patrick Bass · Jun 6 · 21 · 1 Locked
[Major] [High Priority] [Bug Fixed] [Always Reproduces]
🚀 OP Jun 6, 2026 7:18pm

Area: mobieusAI (audit p11) · Surface: AI — mobieusAI community (legacy AiController) · Dimension: security · Severity: major

OWASP A01:2021 Broken Access Control (IDOR). An authenticated standard user can summarize forum threads they cannot view by guessing/enumerating thread IDs against /api/ai/thread-summary/{id}. The summary and extracted key points leak the private thread's actual content (the LLM reads up to 60 posts of the thread body). Per-tenant DB isolation does not help here — this is an intra-tenant private/hidden-forum leak, exactly the gap the rewritten MemberAI endpoint added a guard for.

Evidence

platform/src/Controllers/AiController.php:37-52 summarizeThread() does only requireAuth()+validateCsrf()+flag gate, then calls AiThreadSummary::forThread((int)$id). AiThreadSummary::forThread (platform/src/Services/AiThreadSummary.php:19-100) loads the thread by id and pulls posts with NO Forum::canView / forum-visibility check: line 24 `SELECT id, title, reply_count FROM forum_threads WHERE id = :id` and line 49 `SELECT u.username, p.body ... FROM forum_posts p ... WHERE p.thread_id = :tid`. Contrast with the newer MemberAIController@summarizeThread (platform/src/Controllers/MemberAIController.php:45-48) which DOES gate: `if (!\App\Models\Forum::canView((int)$thread['forum_id'], ...) || \App\Models\Forum::isPreviewOnly(...)) { json(...thread_not_found,404); }`. The route is live in the authenticated group (routes.php:1094) so any role-2 member can POST a guessed thread id and receive an AI-generated summary + key_points of a private/hidden forum thread's contents. The file's own header comment (AiController.php:21-28) calls these methods LEGACY but they remain routed.

Suggested fix. Add the same intra-tenant visibility gate the MemberAI path uses before calling AiThreadSummary::forThread: resolve forum_id for the thread and reject with 404 when !Forum::canView(forumId, userId, role) || Forum::isPreviewOnly(...). Pass the caller's id/role into AiThreadSummary::forThread (or move the check into the service). Better: retire the legacy route entirely (the header comment already says do not wire new UI to it) by removing routes.php:1094-1095 since MemberAI supersedes it.

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


Patrick Bass
@mobieus

🚀 Jun 7, 2026 5:25am

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

Added the intra-tenant visibility gate to summarizeThread(): it now resolves the thread's forum_id and returns 404 (reason=unavailable) when the thread is missing or !Forum::canView(forumId, userId, role) || Forum::isPreviewOnly(...), mirroring MemberAIController so a member cannot summarize hidden/private-forum threads by guessing ids. Since the legacy service AiThreadSummary::forThread(int) takes no caller context and lives in another file, the gate is enforced in the controller before the service call.

Status: fixed. Thread closed and locked.


Patrick Bass
@mobieus

Log in or register to reply to this thread.