Forums Bug Reports Thread

togglePin lets a user pin any thread by ID with no ownership/visibility check — leaks restricted thread titles + working links on a public profile (IDOR / broken access control)

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

Area: Account (re-run) (audit p1r) · Surface: POST /profile/pin (ProfileController@togglePin) · Dimension: security · Severity: major

Any authenticated user can POST thread_id=<any id> to /profile/pin. There is no validation that the thread is theirs or that it sits in a forum they (or the public) may view. ForumThread::findManyByIds resolves the thread regardless of forum visibility or deletion, and the profile page renders the title and a working /thread/<slug> link to every visitor. An attacker can enumerate/guess thread IDs from staff-only, hidden, private-community, or soft-deleted forums, pin them, and harvest the otherwise-restricted titles plus a clickable link from their own public profile. This is an Insecure Direct Object Reference (OWASP A01:2021 Broken Access Control) and an information-disclosure of restricted content.

Evidence

ProfileController.php:600-628 reads only `$threadId = (int) ($_POST['thread_id'] ?? 0)` and pushes it straight into the user's pinned_posts JSON with NO check that the thread exists, belongs to the user, or lives in a forum the user can see: `} else { $pinned[] = $threadId; ... } User::update((int) $user['id'], ['pinned_posts' => json_encode(array_values($pinned))]);`. The pins are rendered on the PUBLIC profile via ForumThread::findManyByIds (ForumThread.php:15-36), whose query is `SELECT t.*, ... FROM forum_threads t JOIN users u ON u.id = t.user_id WHERE t.id IN (...)` — no forum visibility filter, no is_deleted filter, no min_role_view check. The profile template then prints the title + slug for anyone: templates/profile/show.php:856 `<a href="/thread/<?= $e($pt['slug']) ?>"...><?= $e($pt['title']) ?></a>`.

Suggested fix. In togglePin, before adding a thread to the pin list, load the thread and verify ownership (t.user_id === current user) OR that the thread is in a forum the current user is permitted to read; reject otherwise. Additionally, harden ForumThread::findManyByIds (or use a viewer-scoped variant) so the public profile only renders threads in publicly-viewable, non-deleted forums.

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.

Hardened ProfileController::togglePin so a thread can only be pinned if the current user owns it OR can read its forum (ForumThread::findById + Forum::canView with the caller's id/role); unknown/forbidden thread ids are rejected. Unpinning stays unconditional. The optional read-path hardening of ForumThread::findManyByIds lives in a different file (out of scope here), but the write-path gate closes the actual injection vector for this surface.

Status: fixed. Thread closed and locked.


Patrick Bass
@mobieus

Log in or register to reply to this thread.