From a332d22123d04fef4d4f60852be5e1ac2052076c Mon Sep 17 00:00:00 2001 From: Muhamed Fazal PS Date: Fri, 17 Jul 2026 14:28:37 +0530 Subject: [PATCH] fix: remove |safe from striptags output in search templates Four search include templates used |safe on striptags output, which Django's docs explicitly warn against. The indexed value is now genuinely plain text at index time via html.unescape(), so autoescaping can safely render it. Closes python/pythondotorg#3046 --- apps/downloads/search_indexes.py | 3 ++- apps/events/search_indexes.py | 6 ++++-- apps/jobs/search_indexes.py | 4 +++- apps/pages/search_indexes.py | 4 +++- templates/search/includes/downloads.release.html | 2 +- templates/search/includes/events.calendar.html | 2 +- templates/search/includes/events.event.html | 2 +- templates/search/includes/jobs.job.html | 2 +- 8 files changed, 16 insertions(+), 9 deletions(-) diff --git a/apps/downloads/search_indexes.py b/apps/downloads/search_indexes.py index c56dc1e06..2bc71acdb 100644 --- a/apps/downloads/search_indexes.py +++ b/apps/downloads/search_indexes.py @@ -1,6 +1,7 @@ """Haystack search indexes for the downloads app.""" import datetime +import html from django.template.defaultfilters import striptags, truncatewords_html from django.utils import timezone @@ -43,7 +44,7 @@ def prepare_version(self, obj): def prepare_description(self, obj): """Return a truncated plain-text description.""" - return striptags(truncatewords_html(obj.content.rendered, 50)) + return html.unescape(striptags(truncatewords_html(obj.content.rendered, 50))) def prepare(self, obj): """Boost recent releases.""" diff --git a/apps/events/search_indexes.py b/apps/events/search_indexes.py index 827a2309f..3d3413e31 100644 --- a/apps/events/search_indexes.py +++ b/apps/events/search_indexes.py @@ -1,5 +1,7 @@ """Haystack search indexes for the events app.""" +import html + from django.template.defaultfilters import striptags, truncatewords_html from haystack import indexes @@ -28,7 +30,7 @@ def prepare_path(self, obj): def prepare_description(self, obj): """Return a truncated plain-text description.""" - return striptags(truncatewords_html(obj.description, 50)) + return html.unescape(striptags(truncatewords_html(obj.description, 50))) def prepare_include_template(self, obj): """Return the search result template path.""" @@ -65,7 +67,7 @@ def prepare_path(self, obj): def prepare_description(self, obj): """Return a truncated plain-text description.""" - return striptags(truncatewords_html(obj.description.rendered, 50)) + return html.unescape(striptags(truncatewords_html(obj.description.rendered, 50))) def prepare_venue(self, obj): """Return the venue name or None if no venue is set.""" diff --git a/apps/jobs/search_indexes.py b/apps/jobs/search_indexes.py index 48a712849..f2285200c 100644 --- a/apps/jobs/search_indexes.py +++ b/apps/jobs/search_indexes.py @@ -1,5 +1,7 @@ """Haystack search indexes for the jobs app.""" +import html + from django.template.defaultfilters import striptags, truncatewords_html from django.urls import reverse from haystack import indexes @@ -101,7 +103,7 @@ def prepare_include_template(self, obj): def prepare_description(self, obj): """Return a truncated plain-text job description.""" - return striptags(truncatewords_html(obj.description.rendered, 50)) + return html.unescape(striptags(truncatewords_html(obj.description.rendered, 50))) def prepare_path(self, obj): """Return the URL for this job listing.""" diff --git a/apps/pages/search_indexes.py b/apps/pages/search_indexes.py index f94497773..e419f1d97 100644 --- a/apps/pages/search_indexes.py +++ b/apps/pages/search_indexes.py @@ -1,5 +1,7 @@ """Haystack search indexes for the pages app.""" +import html + from django.template.defaultfilters import striptags, truncatewords_html from haystack import indexes @@ -27,7 +29,7 @@ def prepare_description(self, obj): """Create a description if none exists.""" if obj.description: return obj.description - return striptags(truncatewords_html(obj.content.rendered, 50)) + return html.unescape(striptags(truncatewords_html(obj.content.rendered, 50))) def index_queryset(self, using=None): """Only index published pages.""" diff --git a/templates/search/includes/downloads.release.html b/templates/search/includes/downloads.release.html index f2f808d7d..b55efedac 100644 --- a/templates/search/includes/downloads.release.html +++ b/templates/search/includes/downloads.release.html @@ -1,7 +1,7 @@

Release – {{ result.name }}

Version: {{ result.version }}

Released: {{ result.release_date|date }}

-

{{ result.description|safe }}

+

{{ result.description }}

{% if result.release_notes_url %}

View Release Notes

{% endif %} diff --git a/templates/search/includes/events.calendar.html b/templates/search/includes/events.calendar.html index 9fb6ff730..473c7cb21 100644 --- a/templates/search/includes/events.calendar.html +++ b/templates/search/includes/events.calendar.html @@ -1,5 +1,5 @@

Calendar – {{ result.name }}

-

{{ result.description|safe }}

+

{{ result.description }}

{% if result.rss %}

RSS Feed

{% endif %} diff --git a/templates/search/includes/events.event.html b/templates/search/includes/events.event.html index f015b5513..43e859e32 100644 --- a/templates/search/includes/events.event.html +++ b/templates/search/includes/events.event.html @@ -25,4 +25,4 @@

Event – {{ result.name }}

Location: {{ result.venue }}

{% endif %} -

{{ result.description|safe }}

\ No newline at end of file +

{{ result.description }}

\ No newline at end of file diff --git a/templates/search/includes/jobs.job.html b/templates/search/includes/jobs.job.html index 7dd2c0d68..b65c2ee40 100644 --- a/templates/search/includes/jobs.job.html +++ b/templates/search/includes/jobs.job.html @@ -3,4 +3,4 @@

Job - {{ result.name }}

Location: {{ result.city }}{% if result.region %}, {{ result.region }}{% endif %} {{ result.country }}

{% if result.telecommuting %}

Telecommuting: Yes

{% endif %} -

{{ result.description|safe }}

\ No newline at end of file +

{{ result.description }}

\ No newline at end of file