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'{tag}>' 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)