From cdaf6e3aa607645fd8b3a3a73415fd1cd5916a4a Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 13 Oct 2026 13:40:31 +1100 Subject: [PATCH] More robust HTML sanitizing in reports (#12997) --- .../InvenTree/report/templatetags/report.py | 13 +++++++-- src/backend/InvenTree/report/test_tags.py | 29 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/backend/InvenTree/report/templatetags/report.py b/src/backend/InvenTree/report/templatetags/report.py index e00d6144b6..ad015b4784 100644 --- a/src/backend/InvenTree/report/templatetags/report.py +++ b/src/backend/InvenTree/report/templatetags/report.py @@ -18,10 +18,12 @@ from django.core.files.storage import default_storage from django.db.models import Model from django.db.models.query import QuerySet from django.utils import translation +from django.utils.html import conditional_escape, format_html from django.utils.safestring import SafeString, mark_safe from django.utils.translation import gettext_lazy as _ import lxml.html +import nh3 from babel import Locale from babel.core import UnknownLocaleError from babel.dates import format_date as babel_format_date @@ -766,9 +768,9 @@ def internal_link(link, text) -> str: # If the base URL is not set, just return the text if not url: logger.warning('Failed to construct absolute URL for internal link') - return text + return conditional_escape(text) - return mark_safe(f'{text}') + return format_html('{}', url, text) def make_decimal(value: Any) -> Any: @@ -1116,6 +1118,7 @@ def render_html_text(text: str, **kwargs): bold: Boolean, whether bold (or not) italic: Boolean, whether italic (or not) heading: str, heading level e.g. 'h3' + sanitize: Boolean, whether to sanitize the provided text (default = True) """ tags = [] @@ -1128,6 +1131,12 @@ def render_html_text(text: str, **kwargs): if heading := kwargs.get('heading', ''): tags.append(heading) + text = str(text) + + if kwargs.get('sanitize', True): + # Remove any unsafe HTML content (e.g. script tags) from the provided text + text = nh3.clean(text) + output = ''.join([f'<{tag}>' for tag in tags]) output += text output += ''.join([f'' for tag in tags]) diff --git a/src/backend/InvenTree/report/test_tags.py b/src/backend/InvenTree/report/test_tags.py index 4845c5155d..2a801a1c38 100644 --- a/src/backend/InvenTree/report/test_tags.py +++ b/src/backend/InvenTree/report/test_tags.py @@ -326,6 +326,17 @@ class ReportTagTest(PartImageTestMixin, InvenTreeTestCase): link = report_tags.internal_link(None, None) self.assertEqual(link, 'None') + # Unsafe text content is escaped + payload = '">' + escaped = '"><script>alert(1)</script>' + + link = report_tags.internal_link(obj.get_absolute_url(), payload) + self.assertIn(f'>{escaped}', link) + self.assertNotIn('' + ) + + self.assertEqual( + report_tags.render_html_text(payload, bold=True), + 'hello', + ) + + # Encoded HTML entities must not be decoded into live tags + self.assertEqual( + report_tags.render_html_text('<script>'), '<script>' + ) + + # Sanitization can be explicitly disabled + self.assertEqual(report_tags.render_html_text(payload, sanitize=False), payload) + def test_format_date(self): """Test the format_date template tag.""" dt = timezone.datetime(year=2024, month=3, day=13)