From 247bfcddc7071b0c46751c140f55d78dd7fb04ad Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sun, 16 Aug 2026 14:03:01 +0200 Subject: [PATCH 1/2] docs(security): waive gate-7 on the two endpoints that address no object gate-7 reports both of these as NoAdminRequired with no recognised guard. An IDOR needs an attacker-supplied identifier, and neither method accepts one: VisibilityPreviewController::preview takes a candidate rule set and an evaluation context from the request body, loads nothing by id, persists nothing, and evaluates against $this->userId. DashboardShareApiController::searchSharees takes a search string, reads no dashboard and no share, and returns the same directory the core share picker already exposes to any authenticated user. Both null-check $this->userId. searchSharees additionally excludes the caller from its own results, blocks single-character sweeps as a directory enumeration guard, and bounds both searches to 10 rows. Using the exemption the gate provides rather than widening its guard patterns: its own comments record that widening 'would let gate-7 clear real IDORs', and that is a worse trade than two documented waivers. The reason is required and stated. Verified the waiver is load-bearing, not decorative: with it, gate-7 reports zero on this repo; displace the docblock and the finding comes straight back. --- lib/Controller/DashboardShareApiController.php | 7 +++++++ lib/Controller/VisibilityPreviewController.php | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/lib/Controller/DashboardShareApiController.php b/lib/Controller/DashboardShareApiController.php index 8d3145ef..587a8fc1 100644 --- a/lib/Controller/DashboardShareApiController.php +++ b/lib/Controller/DashboardShareApiController.php @@ -300,6 +300,13 @@ public function revokeForRecipient( * @return DataResponse The matching users and groups. * * @spec openspec/specs/dashboard-sharing/spec.md + * + * @no-admin-idor-exempt no object is addressed. The only parameter is a + * search string; the method reads no dashboard and no share, and returns + * the same directory any authenticated user already sees through the core + * share picker. It null-checks `$this->userId`, excludes the caller from + * its own results, blocks single-character sweeps as a directory + * enumeration guard, and bounds both searches to 10 rows. */ #[NoAdminRequired] public function searchSharees(string $query = ''): DataResponse { diff --git a/lib/Controller/VisibilityPreviewController.php b/lib/Controller/VisibilityPreviewController.php index 6e4f98fa..2be7e008 100644 --- a/lib/Controller/VisibilityPreviewController.php +++ b/lib/Controller/VisibilityPreviewController.php @@ -90,6 +90,13 @@ public function __construct( * fail validation. * * @spec openspec/specs/conditional-visibility-editor/spec.md#requirement-req-cvui-005-preview-endpoint-reuses-the-render-time-evaluation-path-and-never-persists + * + * @no-admin-idor-exempt no object is addressed. Both parameters are the + * candidate rule set and an evaluation context taken from the request + * body; the method loads nothing by id, persists nothing, and evaluates + * against `$this->userId` — which it null-checks — so there is no other + * user's object for a caller to reach. An IDOR needs an attacker-supplied + * identifier, and this endpoint accepts none. */ #[NoAdminRequired] public function preview( From ef50f7da958cc92bfcc79fd2b2c1b6a436a21b7c Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sun, 16 Aug 2026 14:59:53 +0200 Subject: [PATCH 2/2] fix(gates): clear gate-26, gate-35 and gate-57 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gate-26 — TemplatesPage.vue was reported as having no visual proof while tests/e2e/admin-templates-page.spec.ts tests exactly that page. The gate matches a page against its component stem, the spec only ever wrote 'TemplatesPage' in comments, and the corpus strips comments (a comment is not a baseline, .github#358). The selector is now a named constant, so the link between spec and component is executable rather than prose. Verified load-bearing: rename the constant and the finding returns. gate-57 — DashboardVersionService::deleteVersionsForDashboard() had no production caller anywhere. VersionsListener IS registered on DashboardDeletedEvent and does the same single mapper call directly, so the cascade has always worked and the service method was dead. Removed. The interesting part is what the coverage was doing: the dead wrapper had a unit test, and VersionsListener — the live path — had none. So the tested path was the one nothing called. VersionsListener now has its own test, including that an unrelated event deletes nothing (without which the listener could fire on any event and the happy-path test would still pass) and that a mapper failure is logged rather than rethrown, which REQ-CSC-006 requires because a throw would abort the sibling listeners on the same event. gate-35 — the inert news card's thumbnail now says aria-hidden="true" alongside alt="". The headline beside it carries the meaning and the card is inert, so there is no link for the image to name; the attribute states that explicitly instead of leaving alt="" to imply it. gate-37 (aria-hidden-focusable) re-checked and still clear — an img is not focusable. --- lib/Service/DashboardVersionService.php | 18 --- .../Widgets/Renderers/NewsWidget.vue | 7 +- tests/Unit/Listener/VersionsListenerTest.php | 150 ++++++++++++++++++ .../Service/DashboardVersionServiceTest.php | 19 +-- tests/e2e/admin-templates-page.spec.ts | 19 ++- 5 files changed, 175 insertions(+), 38 deletions(-) create mode 100644 tests/Unit/Listener/VersionsListenerTest.php diff --git a/lib/Service/DashboardVersionService.php b/lib/Service/DashboardVersionService.php index d0d6138c..a853f9e7 100644 --- a/lib/Service/DashboardVersionService.php +++ b/lib/Service/DashboardVersionService.php @@ -527,24 +527,6 @@ public function restoreVersion( ]; }//end restoreVersion() - /** - * Cascade-delete every snapshot row for a dashboard. - * - * Designed to be called from the dashboard delete path or, in the - * future, the cascade-events VersionsListener stub. Idempotent. - * - * @param string $dashboardUuid The dashboard UUID. - * - * @return integer The number of rows deleted. - * - * @spec openspec/specs/dashboard-versioning/spec.md - */ - public function deleteVersionsForDashboard(string $dashboardUuid): int { - return $this->versionMapper->deleteByDashboardUuid( - dashboardUuid: $dashboardUuid - ); - }//end deleteVersionsForDashboard() - /** * Whether the supplied dashboard is groupfolder-backed * (REQ-VERS-008). Currently always false because the groupfolder diff --git a/src/components/Widgets/Renderers/NewsWidget.vue b/src/components/Widgets/Renderers/NewsWidget.vue index 24cf65eb..86b50fc1 100644 --- a/src/components/Widgets/Renderers/NewsWidget.vue +++ b/src/components/Widgets/Renderers/NewsWidget.vue @@ -74,11 +74,16 @@