1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2026-01-13 05:42:30 +03:00

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.
This commit is contained in:
Dan Brown
2025-12-30 13:29:04 +00:00
parent 88d86df66f
commit b08d1b36de
4 changed files with 82 additions and 2 deletions

View File

@@ -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);
}

View File

@@ -82,4 +82,12 @@ class SearchOptionSet
$values = array_values(array_filter($this->options, fn (SearchOption $option) => !$option->negated));
return new self($values);
}
/**
* @return self<T>
*/
public function limit(int $limit): self
{
return new self(array_slice(array_values($this->options), 0, $limit));
}
}

View File

@@ -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.
*/

View File

@@ -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());
}
}