diff --git a/docs/ref/contrib/postgres/search.txt b/docs/ref/contrib/postgres/search.txt index 220ab4c591..e1459847f1 100644 --- a/docs/ref/contrib/postgres/search.txt +++ b/docs/ref/contrib/postgres/search.txt @@ -218,6 +218,19 @@ Usage example: See :ref:`postgresql-fts-search-configuration` for an explanation of the ``config`` parameter. +.. warning:: + + In order to prevent XSS attacks, the string value of this field is not + marked as safe. Although the ``start_sel`` and ``stop_sel`` parameters + default to ```` and ````, it is dangerous to mark the headline as + safe or to pass it to the :tfilter:`safe` template filter, especially if + the value is from an untrusted source. + + Tested versions of PostgreSQL will not remove or escape HTML tags from the + headline if ``highlight_all`` is ``True``. If ``highlight_all`` is + ``False``, then HTML tags will be removed from the headline. This behavior + is undocumented, and must not be relied upon. + .. _highlighting search results: https://www.postgresql.org/docs/current/textsearch-controls.html#TEXTSEARCH-HEADLINE .. _postgresql-fts-search-configuration: diff --git a/tests/postgres_tests/test_search.py b/tests/postgres_tests/test_search.py index 472dca6c7b..2ff2ab5c4a 100644 --- a/tests/postgres_tests/test_search.py +++ b/tests/postgres_tests/test_search.py @@ -7,6 +7,7 @@ transcript. """ from django.db.models import F, Value +from django.utils.safestring import SafeString from . import PostgreSQLSimpleTestCase, PostgreSQLTestCase from .models import Character, Line, LineSavedSearch, Scene @@ -732,6 +733,42 @@ class SearchHeadlineTests(GrailTestData, PostgreSQLTestCase): searched.headline, ) + def test_headline_sanitize_html(self): + dangerous_line = Line.objects.create( + scene=self.robin, + character=self.minstrel, + dialogue='Foobar ', + ) + dangerous_line.full_clean() + + searched = Line.objects.annotate( + headline=SearchHeadline( + "dialogue", + SearchQuery("Foobar", config="english"), + highlight_all=False, + ), + ).get(pk=dangerous_line.pk) + self.assertNotIsInstance(searched.headline, SafeString) + self.assertEqual( + searched.headline, + 'Foobar console.log("danger"); ', + "When hightlight_all is False, PostgreSQL removes existing HTML tags", + ) + + searched = Line.objects.annotate( + headline=SearchHeadline( + "dialogue", + SearchQuery("Foobar", config="english"), + highlight_all=True, + ), + ).get(pk=dangerous_line.pk) + self.assertNotIsInstance(searched.headline, SafeString) + self.assertEqual( + searched.headline, + 'Foobar ', + "When highlight_all is True, PostgreSQL riskily keeps existing HTML tags", + ) + def test_headline_short_word_option(self): self.check_default_text_search_config() searched = Line.objects.annotate(