From 5b4c6b58a097028de970875605680df941ab0a47 Mon Sep 17 00:00:00 2001
From: Carlton Gibson <carlton.gibson@noumenal.es>
Date: Thu, 22 Aug 2019 14:09:49 +0200
Subject: [PATCH] Fixed #30064 -- Added form to validate admin search fields
 query input.

---
 django/contrib/admin/views/main.py | 19 ++++++++++++++++++-
 tests/admin_changelist/tests.py    | 17 +++++++++++++++++
 2 files changed, 35 insertions(+), 1 deletion(-)

diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py
index 7d8d33d9d7..89b782ff75 100644
--- a/django/contrib/admin/views/main.py
+++ b/django/contrib/admin/views/main.py
@@ -1,6 +1,8 @@
 from datetime import datetime, timedelta
 
+from django import forms
 from django.conf import settings
+from django.contrib import messages
 from django.contrib.admin import FieldListFilter
 from django.contrib.admin.exceptions import (
     DisallowedModelAdminLookup, DisallowedModelAdminToField,
@@ -34,7 +36,18 @@ IGNORED_PARAMS = (
     ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR, TO_FIELD_VAR)
 
 
+class ChangeListSearchForm(forms.Form):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        # Populate "fields" dynamically because SEARCH_VAR is a variable:
+        self.fields = {
+            SEARCH_VAR: forms.CharField(required=False, strip=False),
+        }
+
+
 class ChangeList:
+    search_form_class = ChangeListSearchForm
+
     def __init__(self, request, model, list_display, list_display_links,
                  list_filter, date_hierarchy, search_fields, list_select_related,
                  list_per_page, list_max_show_all, list_editable, model_admin, sortable_by):
@@ -56,6 +69,11 @@ class ChangeList:
         self.sortable_by = sortable_by
 
         # Get search parameters from the query string.
+        _search_form = self.search_form_class(request.GET)
+        if not _search_form.is_valid():
+            for error in _search_form.errors.values():
+                messages.error(request, ', '.join(error))
+        self.query = _search_form.cleaned_data.get(SEARCH_VAR) or ''
         try:
             self.page_num = int(request.GET.get(PAGE_VAR, 0))
         except ValueError:
@@ -76,7 +94,6 @@ class ChangeList:
             self.list_editable = ()
         else:
             self.list_editable = list_editable
-        self.query = request.GET.get(SEARCH_VAR, '')
         self.queryset = self.get_queryset(request)
         self.get_results(request)
         if self.is_popup:
diff --git a/tests/admin_changelist/tests.py b/tests/admin_changelist/tests.py
index c44fbcc875..05490b061a 100644
--- a/tests/admin_changelist/tests.py
+++ b/tests/admin_changelist/tests.py
@@ -8,6 +8,7 @@ from django.contrib.admin.tests import AdminSeleniumTestCase
 from django.contrib.admin.views.main import ALL_VAR, SEARCH_VAR
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
+from django.contrib.messages.storage.cookie import CookieStorage
 from django.db import connection, models
 from django.db.models import F
 from django.db.models.fields import Field, IntegerField
@@ -406,6 +407,22 @@ class ChangeListTests(TestCase):
         # Make sure distinct() was called
         self.assertEqual(cl.queryset.count(), 1)
 
+    def test_changelist_search_form_validation(self):
+        m = ConcertAdmin(Concert, custom_site)
+        tests = [
+            ({SEARCH_VAR: '\x00'}, 'Null characters are not allowed.'),
+            ({SEARCH_VAR: 'some\x00thing'}, 'Null characters are not allowed.'),
+        ]
+        for case, error in tests:
+            with self.subTest(case=case):
+                request = self.factory.get('/concert/', case)
+                request.user = self.superuser
+                request._messages = CookieStorage(request)
+                m.get_changelist_instance(request)
+                messages = [m.message for m in request._messages]
+                self.assertEqual(1, len(messages))
+                self.assertEqual(error, messages[0])
+
     def test_distinct_for_non_unique_related_object_in_search_fields(self):
         """
         Regressions tests for #15819: If a field listed in search_fields