Area: mobieusKnow (audit p6) · Surface: GET /know/{slug}/history/{id} (KnowledgeController@historyRevision) · Dimension: security · Severity: critical
OWASP A01:2021 Broken Access Control. The trust/moderation model holds untrusted users' edits in a 'pending' queue and discards 'rejected' edits, but the per-revision view exposes those exact bodies to everyone, including anonymous users. Content a moderator rejected (spam, doxxing, defamation, unpublished drafts) is publicly retrievable by id. There is no authentication, no role gate, and no status gate on this read path.
Evidence
KnowledgeController.php:277-313 historyRevision() has NO requireAuth() and is registered in the PUBLIC route group (routes.php:590, group starts routes.php:340, before the auth group at 775). It only validates page_id match: `if (!$rev || (int) $rev['page_id'] !== (int) $kp['id']) { $this->abortNotFound(); }` — no status check. KnowledgeRevision::findById (KnowledgeRevision.php:31-41) selects `kr.*` with `WHERE kr.id = :id` and NO status filter. revision.php:12,78 then renders the body: `$newBody = (string) ($rev['content_markdown'] ?? '');` ... `$renderedHtml = ... MarkdownService::renderUserContent($newBody)`. So an unauthenticated visitor who enumerates revision ids (`/know/<slug>/history/<id>`) reads the full markdown of pending edits awaiting moderator review AND rejected edits that were never published.
Suggested fix. In historyRevision(): require auth, and unless the viewer is a moderator (KnowledgeTrust::isModerator || role>=4) restrict $rev to approved/rolled_back statuses (return 404 for pending/rejected). Mirror this in KnowledgeRevision::findById or add a status-aware lookup used by the public path.
Filed by the automated tenant-app audit and adversarially evidence-verified. Status: verified. Open — not yet actioned.
Patrick Bass
@mobieus