fix(search): cap the global search query too

GlobalSearchView calls the backend directly rather than through the shared
helper the cap lives in, so "every query string is length-checked" was a
claim with an exception rather than an invariant.

It hardcodes SearchMode.TEXT, which is linear rather than quadratic, so
this path was never the CPU-exhaustion vector and this is not a fix for
one. It is capped so the invariant holds without a footnote: the view
already bounds the query from below, and a later change letting it select
a search mode would otherwise reopen the hole with nothing to catch it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
stumpylog
2026-08-20 11:37:28 -07:00
co-authored by Claude Opus 5
parent 885bc2fdf3
commit 6ab4c3d689
2 changed files with 38 additions and 0 deletions
@@ -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
+4
View File
@@ -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)