From 4805a64c4cd536f72175b7c43f96c8c599a73e70 Mon Sep 17 00:00:00 2001
From: apoorva-01
Date: Wed, 8 Jul 2026 06:08:07 +0530
Subject: [PATCH] Don't mark striptags output as safe in search results
striptags output isn't HTML-safe, so |safe on it is exactly what Django's
docs warn against. Dropping |safe alone would double-escape (strip_tags
leaves entities encoded), so unescape the description at index time and
let autoescaping do the work.
---
apps/downloads/search_indexes.py | 3 +-
apps/downloads/tests/test_search_indexes.py | 22 +++++++++++++
apps/events/search_indexes.py | 6 ++--
apps/events/tests/test_search_indexes.py | 31 +++++++++++++++++++
apps/jobs/search_indexes.py | 4 ++-
apps/jobs/tests/test_search_indexes.py | 24 ++++++++++++++
apps/pages/search_indexes.py | 4 ++-
apps/pages/tests/test_search_indexes.py | 20 ++++++++++++
.../search/includes/downloads.release.html | 2 +-
.../search/includes/events.calendar.html | 2 +-
templates/search/includes/events.event.html | 2 +-
templates/search/includes/jobs.job.html | 2 +-
12 files changed, 113 insertions(+), 9 deletions(-)
create mode 100644 apps/downloads/tests/test_search_indexes.py
create mode 100644 apps/events/tests/test_search_indexes.py
create mode 100644 apps/jobs/tests/test_search_indexes.py
create mode 100644 apps/pages/tests/test_search_indexes.py
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/downloads/tests/test_search_indexes.py b/apps/downloads/tests/test_search_indexes.py
new file mode 100644
index 000000000..15bfa27d6
--- /dev/null
+++ b/apps/downloads/tests/test_search_indexes.py
@@ -0,0 +1,22 @@
+from types import SimpleNamespace
+
+from django.template.loader import render_to_string
+from django.test import SimpleTestCase
+
+from apps.downloads.search_indexes import ReleaseIndex
+
+
+class ReleaseSearchIndexTests(SimpleTestCase):
+ def test_prepare_description_returns_plain_text(self):
+ obj = SimpleNamespace(content=SimpleNamespace(rendered="Data & AI role
"))
+
+ self.assertEqual(ReleaseIndex().prepare_description(obj), "Data & AI role alert(1)")
+
+ def test_result_template_escapes_description(self):
+ rendered = render_to_string(
+ "search/includes/downloads.release.html",
+ {"result": {"description": ""}},
+ )
+
+ self.assertNotIn("
")
+ )
+
+ self.assertEqual(EventIndex().prepare_description(obj), "Data & AI role alert(1)")
+
+ def test_calendar_prepare_description_returns_plain_text(self):
+ obj = SimpleNamespace(description="Data & AI role
")
+
+ self.assertEqual(CalendarIndex().prepare_description(obj), "Data & AI role alert(1)")
+
+ def test_result_templates_escape_description(self):
+ for template in (
+ "search/includes/events.event.html",
+ "search/includes/events.calendar.html",
+ ):
+ with self.subTest(template=template):
+ rendered = render_to_string(template, {"result": {"description": ""}})
+
+ self.assertNotIn("")
+ )
+
+ self.assertEqual(JobIndex().prepare_description(obj), "Data & AI role alert(1)")
+
+ def test_result_template_escapes_description(self):
+ rendered = render_to_string(
+ "search/includes/jobs.job.html",
+ {"result": {"description": ""}},
+ )
+
+ self.assertNotIn(""),
+ )
+
+ self.assertEqual(PageIndex().prepare_description(obj), "Data & AI role alert(1)")
+
+ def test_prepare_description_keeps_existing_description(self):
+ obj = SimpleNamespace(description="Kept & raw", content=None)
+
+ self.assertEqual(PageIndex().prepare_description(obj), "Kept & raw")
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 @@
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 @@
-{{ 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 @@
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 @@
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