From b08d1b36de36d96fae55fff65bcb5908a43e63b5 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 30 Dec 2025 13:29:04 +0000 Subject: [PATCH] Search: Set limits on the amount of search terms Sets some reasonable limits, which are higher when logged in since that infers a little extra trust. Helps prevent against large resource consuption attacks via super heavy search queries. Thanks to Gabriel Rodrigues AKA TEXUGO for reporting. --- app/Search/SearchController.php | 5 +-- app/Search/SearchOptionSet.php | 8 +++++ app/Search/SearchOptions.php | 22 ++++++++++++++ tests/Search/SearchOptionsTest.php | 49 ++++++++++++++++++++++++++++++ 4 files changed, 82 insertions(+), 2 deletions(-) diff --git a/app/Search/SearchController.php b/app/Search/SearchController.php index 8a6a5bbde..348d44a42 100644 --- a/app/Search/SearchController.php +++ b/app/Search/SearchController.php @@ -78,8 +78,9 @@ class SearchController extends Controller // Search for entities otherwise show most popular if ($searchTerm !== false) { - $searchTerm .= ' {type:' . implode('|', $entityTypes) . '}'; - $entities = $this->searchRunner->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 20)['results']; + $options = SearchOptions::fromString($searchTerm); + $options->setFilter('type', implode('|', $entityTypes)); + $entities = $this->searchRunner->searchEntities($options, 'all', 1, 20)['results']; } else { $entities = $queryPopular->run(20, 0, $entityTypes); } diff --git a/app/Search/SearchOptionSet.php b/app/Search/SearchOptionSet.php index 844d145e6..19f1c5509 100644 --- a/app/Search/SearchOptionSet.php +++ b/app/Search/SearchOptionSet.php @@ -82,4 +82,12 @@ class SearchOptionSet $values = array_values(array_filter($this->options, fn (SearchOption $option) => !$option->negated)); return new self($values); } + + /** + * @return self + */ + public function limit(int $limit): self + { + return new self(array_slice(array_values($this->options), 0, $limit)); + } } diff --git a/app/Search/SearchOptions.php b/app/Search/SearchOptions.php index bf527d9c3..83af2d043 100644 --- a/app/Search/SearchOptions.php +++ b/app/Search/SearchOptions.php @@ -35,6 +35,7 @@ class SearchOptions { $instance = new self(); $instance->addOptionsFromString($search); + $instance->limitOptions(); return $instance; } @@ -87,6 +88,8 @@ class SearchOptions $instance->filters = $instance->filters->merge($extras->filters); } + $instance->limitOptions(); + return $instance; } @@ -147,6 +150,25 @@ class SearchOptions $this->filters = $this->filters->merge(new SearchOptionSet($terms['filters'])); } + /** + * Limit the amount of search options to reasonable levels. + * Provides higher limits to logged-in users since that signals a slightly + * higher level of trust. + */ + protected function limitOptions(): void + { + $userLoggedIn = !user()->isGuest(); + $searchLimit = $userLoggedIn ? 10 : 5; + $exactLimit = $userLoggedIn ? 4 : 2; + $tagLimit = $userLoggedIn ? 8 : 4; + $filterLimit = $userLoggedIn ? 10 : 5; + + $this->searches = $this->searches->limit($searchLimit); + $this->exacts = $this->exacts->limit($exactLimit); + $this->tags = $this->tags->limit($tagLimit); + $this->filters = $this->filters->limit($filterLimit); + } + /** * Decode backslash escaping within the input string. */ diff --git a/tests/Search/SearchOptionsTest.php b/tests/Search/SearchOptionsTest.php index 2ebf273dd..4b0fa0f3a 100644 --- a/tests/Search/SearchOptionsTest.php +++ b/tests/Search/SearchOptionsTest.php @@ -142,4 +142,53 @@ class SearchOptionsTest extends TestCase $this->assertEquals('dino', $options->exacts->all()[0]->value); $this->assertTrue($options->exacts->all()[0]->negated); } + + public function test_from_string_results_are_count_limited_and_larger_for_logged_in_users() + { + $terms = [ + ...array_fill(0, 40, 'cat'), + ...array_fill(0, 50, '"bees"'), + ...array_fill(0, 50, '{is_template}'), + ...array_fill(0, 50, '[a=b]'), + ]; + + $options = SearchOptions::fromString(implode(' ', $terms)); + + $this->assertCount(5, $options->searches->all()); + $this->assertCount(2, $options->exacts->all()); + $this->assertCount(4, $options->tags->all()); + $this->assertCount(5, $options->filters->all()); + + $this->asEditor(); + $options = SearchOptions::fromString(implode(' ', $terms)); + + $this->assertCount(10, $options->searches->all()); + $this->assertCount(4, $options->exacts->all()); + $this->assertCount(8, $options->tags->all()); + $this->assertCount(10, $options->filters->all()); + } + + public function test_from_request_results_are_count_limited_and_larger_for_logged_in_users() + { + $request = new Request([ + 'search' => str_repeat('hello ', 20), + 'tags' => array_fill(0, 20, 'a=b'), + 'extras' => str_repeat('-[b=c] -{viewed_by_me} -"dino"', 20), + ]); + + $options = SearchOptions::fromRequest($request); + + $this->assertCount(5, $options->searches->all()); + $this->assertCount(2, $options->exacts->all()); + $this->assertCount(4, $options->tags->all()); + $this->assertCount(5, $options->filters->all()); + + $this->asEditor(); + $options = SearchOptions::fromRequest($request); + + $this->assertCount(10, $options->searches->all()); + $this->assertCount(4, $options->exacts->all()); + $this->assertCount(8, $options->tags->all()); + $this->assertCount(10, $options->filters->all()); + } }