mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-08-25 20:23:18 +00:00
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:
co-authored by
Claude Opus 5
parent
885bc2fdf3
commit
6ab4c3d689
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user