From 63031471e24605bebab54115f24686b0a01975db Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 28 Jul 2026 22:23:47 -0400 Subject: [PATCH] authorize read access to collection and navigation trees `CollectionTreeController::index()` and `NavigationTreeController::index()` were reachable by any user with `access cp`, leaking entry titles, slugs, urls, statuses and redirects from collections and navs the user isn't permitted to view. Their `update()` counterparts already authorized. Co-Authored-By: Claude Opus 5 --- .../Collections/CollectionTreeController.php | 2 + .../Navigation/NavigationTreeController.php | 4 ++ .../Collections/ViewCollectionTreeTest.php | 66 +++++++++++++++++++ .../Navigation/ViewNavigationTreeTest.php | 61 +++++++++++++++++ 4 files changed, 133 insertions(+) create mode 100644 tests/Feature/Collections/ViewCollectionTreeTest.php create mode 100644 tests/Feature/Navigation/ViewNavigationTreeTest.php diff --git a/src/Http/Controllers/CP/Collections/CollectionTreeController.php b/src/Http/Controllers/CP/Collections/CollectionTreeController.php index 5b9b722f31a..a2479a6647b 100644 --- a/src/Http/Controllers/CP/Collections/CollectionTreeController.php +++ b/src/Http/Controllers/CP/Collections/CollectionTreeController.php @@ -18,6 +18,8 @@ class CollectionTreeController extends CpController { public function index(Request $request, Collection $collection) { + $this->authorize('view', $collection, trans('You are not authorized to view this collection.')); + $site = $request->site ?? Site::selected()->handle(); $pages = (new TreeBuilder)->buildForController([ diff --git a/src/Http/Controllers/CP/Navigation/NavigationTreeController.php b/src/Http/Controllers/CP/Navigation/NavigationTreeController.php index 67c9a2f4773..663bb10f2dd 100644 --- a/src/Http/Controllers/CP/Navigation/NavigationTreeController.php +++ b/src/Http/Controllers/CP/Navigation/NavigationTreeController.php @@ -11,6 +11,8 @@ use Statamic\Structures\TreeBuilder; use Statamic\Support\Arr; +use function Statamic\trans as __; + class NavigationTreeController extends CpController { private $data; @@ -22,6 +24,8 @@ public function index(Request $request, $handle) $site = $request->site ?? Site::selected()->handle(); + $this->authorize('view', $nav->in($site), __('You are not authorized to view navs.')); + $nav->in($site)->ensureBranchIds(); $pages = (new TreeBuilder)->buildForController([ diff --git a/tests/Feature/Collections/ViewCollectionTreeTest.php b/tests/Feature/Collections/ViewCollectionTreeTest.php new file mode 100644 index 00000000000..dd0e9022d2f --- /dev/null +++ b/tests/Feature/Collections/ViewCollectionTreeTest.php @@ -0,0 +1,66 @@ +setTestRoles(['test' => ['access cp', 'view test entries']]); + $user = tap(User::make()->assignRole('test'))->save(); + $collection = $this->createStructuredCollection(); + + $response = $this + ->actingAs($user) + ->index($collection) + ->assertOk(); + + $this->assertEquals(['e1', 'e2'], collect($response->json('pages'))->map->entry->all()); + } + + #[Test] + public function it_denies_access_if_you_dont_have_permission_to_view_the_collection() + { + $this->setTestRoles(['test' => ['access cp']]); + $user = tap(User::make()->assignRole('test'))->save(); + $collection = $this->createStructuredCollection(); + + $this + ->actingAs($user) + ->index($collection) + ->assertForbidden(); + } + + private function createStructuredCollection() + { + $collection = tap(Collection::make('test')->routes('{parent_uri}/{slug}'))->save(); + + EntryFactory::id('e1')->collection($collection)->slug('a')->create(); + EntryFactory::id('e2')->collection($collection)->slug('b')->create(); + + $collection->structureContents(['root' => false])->save(); + $collection->structure()->in('en')->tree([ + ['entry' => 'e1'], + ['entry' => 'e2'], + ])->save(); + + return $collection; + } + + private function index($collection) + { + return $this->getJson(cp_route('collections.tree.index', $collection->handle())); + } +} diff --git a/tests/Feature/Navigation/ViewNavigationTreeTest.php b/tests/Feature/Navigation/ViewNavigationTreeTest.php new file mode 100644 index 00000000000..36676e18f51 --- /dev/null +++ b/tests/Feature/Navigation/ViewNavigationTreeTest.php @@ -0,0 +1,61 @@ +setTestRoles(['test' => ['access cp', 'view test nav']]); + $user = tap(User::make()->assignRole('test'))->save(); + $nav = $this->createNavWithTree(); + + $response = $this + ->actingAs($user) + ->index($nav) + ->assertOk(); + + $this->assertEquals(['One', 'Two'], collect($response->json('pages'))->map->title->all()); + } + + #[Test] + public function it_denies_access_if_you_dont_have_permission_to_view_the_nav() + { + $this->setTestRoles(['test' => ['access cp']]); + $user = tap(User::make()->assignRole('test'))->save(); + $nav = $this->createNavWithTree(); + + $this + ->actingAs($user) + ->index($nav) + ->assertForbidden(); + } + + private function createNavWithTree() + { + $nav = tap(Nav::make('test'))->save(); + + $nav->makeTree('en', [ + ['id' => 'id1', 'title' => 'One', 'url' => 'http://example.com/one'], + ['id' => 'id2', 'title' => 'Two', 'url' => 'http://example.com/two'], + ])->save(); + + return $nav; + } + + private function index($nav) + { + return $this->getJson(cp_route('navigation.tree.index', $nav->handle())); + } +}