Area: Forums (audit p2) · Surface: GET /forums/{slug}/photos, GET /forums/category/{slug}/photos, GET /photos/album/{id}, GET /photos/serve/{id} (PhotoGalleryController) · Dimension: Broken Access Control / IDOR (OWASP A01:2021) · Severity: major
Photos and videos uploaded to a forum that is hidden or listed_private (or under a category whose min_role_view exceeds the viewer's role) are exposed to any authenticated user. The /forums/{slug}/photos and /forums/category/{slug}/photos listing pages render album titles, descriptions and cover thumbnails without a visibility gate; /photos/album/{id} renders the full album and /photos/serve/{id} streams the actual image/video bytes, both gated only by viewableBy() which treats the default 'members' privacy as 'any logged-in user' rather than 'member of THIS community'. A non-member can enumerate album IDs (or follow the leaked list links) and view private-forum media. The rest of the forum subsystem consistently enforces Forum::canView; these photo surfaces are the gap.
Evidence
platform/src/Controllers/PhotoGalleryController.php:618-655 forumAlbums() and :540-566 communityAlbums() look up the forum/category by slug and render PhotoAlbum::byCommunity() with NO Forum::canView() / category min_role_view check (unlike every other forum surface, e.g. ForumController::showForum:157 and ForumInteractionController::vote:66 which all call Forum::canView). The album-list template renders every album's title + cover image: platform/templates/photos/community-albums.php:100-104 (`<a href="/photos/album/<id>"> ... <img src="<?= $e($coverSrc) ?>">`). The album page and the image bytes gate ONLY on PhotoAlbum::viewableBy(): showAlbum():78 and serve():1187. viewableBy() (platform/src/Models/PhotoAlbum.php:189-210) returns TRUE for privacy='members' for ANY logged-in viewer (line 194 `if ($privacy === 'members') return true;`) and 'members' is the default privacy (createForumAlbum:682 `$privacy = (string)($_POST['privacy'] ?? 'members')`). It never consults forum membership or Forum::canView/isPreviewOnly.
Suggested fix. Before rendering community/forum album lists, album pages, and serving items, resolve the owning community: if community_id maps to a forum, require Forum::canView(forumId, viewerId, role) && !isPreviewOnly(); if it maps to a forum_category, require category min_role_view <= role. Make PhotoAlbum::viewableBy() community-aware: for community albums, 'members' must mean 'passes the owning forum/category visibility + membership check', not 'any authenticated user'. Also route cover images through /photos/serve (with the gate) instead of emitting the raw storage path in the template.
Filed by the automated tenant-app audit and adversarially evidence-verified. Status: verified. Open — not yet actioned.
Patrick Bass
@mobieus