Area: Forums (re-run) (audit p2r) · Surface: GET /tag/{slug} (TagController@show → Tag::recentThreads) · Dimension: Security (OWASP) · Severity: major
Any thread that lives in a hidden, private, or paid (access_rule) forum but carries a public tag is enumerated on the public tag landing page, including its title, its slug (so the canonical /thread/{slug} URL is disclosed), and the restricted forum's name — to logged-out users. This defeats the confidentiality of hidden/paid forums for any tagged thread. OWASP A01:2021 (Broken Access Control — sensitive data exposure via missing object-level authorization in a list query). Distinct from the already-filed photo-album leak (different surface and model).
Evidence
TagController::show (platform/src/Controllers/TagController.php:26-75) has NO requireAuth and no forum-visibility filtering; it calls `Tag::recentThreads((int) $tag['id'], $page, $limit, $sort)`. Tag::recentThreads (platform/src/Models/Tag.php:264-313) selects `ft.title, ft.slug, ... f.name AS forum_name, f.slug AS forum_slug FROM thread_tags tt JOIN forum_threads ft ON ft.id = tt.thread_id JOIN forums f ON f.id = ft.forum_id WHERE tt.tag_id = :t` — joined with NO predicate on f.visibility or f.access_rule and no canView call. Forum::canView (platform/src/Models/Forum.php:330-357) shows 'hidden' forums and access_rule (paid/boosters_only) forums are normally invisible to non-members. The template renders them to everyone: templates/tags/show.php:77 `<a href="/thread/<?= $e($t['slug']) ?>"...><?= $e($t['title']) ?></a>` and :80 the forum name. Output is escaped so no XSS, but titles/slugs/forum names of restricted forums are exposed.
Suggested fix. Add forum-visibility filtering to Tag::recentThreads (and the matching count query): pass the viewer id/role and exclude rows the viewer cannot see — e.g. only include forums where visibility='public' AND access_rule IS NULL for anonymous/non-member viewers, or post-filter each row through Forum::canView. Apply the same fix to any RSS/feed path that reuses recentThreads.
Filed by the automated tenant-app audit and adversarially evidence-verified. Status: verified. Open — not yet actioned.
Patrick Bass
@mobieus