diff --git a/src/documents/tests/test_api_search_query_length.py b/src/documents/tests/test_api_search_query_length.py index 9c2c39647..e94101cb2 100644 --- a/src/documents/tests/test_api_search_query_length.py +++ b/src/documents/tests/test_api_search_query_length.py @@ -149,3 +149,37 @@ class TestPostSelectionPathsEnforceTheCap: message = str(response.data["query"]) assert str(_MAX_QUERY_LENGTH) in message assert str(_MAX_QUERY_LENGTH + 1) in message + + +class TestGlobalSearchEnforcesTheCapToo: + """GlobalSearchView calls the backend directly, not through the shared helper. + + It hardcodes SearchMode.TEXT, which is linear rather than quadratic, so it + was never the CPU-exhaustion vector. It is capped anyway so that "every + user query string reaching the backend passes a length check" is an + invariant rather than a claim with an exception: the view already bounds + the query from below, and a later change letting it select a mode would + otherwise reopen the hole silently. + """ + + def test_query_one_over_the_cap_is_a_400( + self, + admin_client: APIClient, + indexed_document: Document, + ) -> None: + response = admin_client.get( + "/api/search/", + {"query": "a" * (_MAX_QUERY_LENGTH + 1)}, + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_query_at_exactly_the_cap_is_accepted( + self, + admin_client: APIClient, + indexed_document: Document, + ) -> None: + response = admin_client.get( + "/api/search/", + {"query": "a" * _MAX_QUERY_LENGTH}, + ) + assert response.status_code == status.HTTP_200_OK diff --git a/src/documents/views.py b/src/documents/views.py index d4ce37fe3..15284f22c 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -3639,6 +3639,10 @@ class GlobalSearchView(PassUserMixin): return HttpResponseBadRequest("Query required") if len(query) < 3: return HttpResponseBadRequest("Query must be at least 3 characters") + if len(query) > _MAX_QUERY_LENGTH: + return HttpResponseBadRequest( + f"Query must be at most {_MAX_QUERY_LENGTH} characters", + ) db_only = request.query_params.get("db_only", False)