mirror of
https://github.com/inventree/InvenTree.git
synced 2026-10-14 22:20:59 +00:00
More robust HTML sanitizing in reports (#12997)
This commit is contained in:
1 parent
bdcba5cbb9
commit
cdaf6e3aa6
2 files changed
+40
-2
No files matched your search
@@ -18,10 +18,12 @@ from django.core.files.storage import default_storage
|
|||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
from django.utils import translation
|
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.safestring import SafeString, mark_safe
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
import lxml.html
|
import lxml.html
|
||||||
|
import nh3
|
||||||
from babel import Locale
|
from babel import Locale
|
||||||
from babel.core import UnknownLocaleError
|
from babel.core import UnknownLocaleError
|
||||||
from babel.dates import format_date as babel_format_date
|
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 the base URL is not set, just return the text
|
||||||
if not url:
|
if not url:
|
||||||
logger.warning('Failed to construct absolute URL for internal link')
|
logger.warning('Failed to construct absolute URL for internal link')
|
||||||
return text
|
return conditional_escape(text)
|
||||||
|
|
||||||
return mark_safe(f'<a href="{url}">{text}</a>')
|
return format_html('<a href="{}">{}</a>', url, text)
|
||||||
|
|
||||||
|
|
||||||
def make_decimal(value: Any) -> Any:
|
def make_decimal(value: Any) -> Any:
|
||||||
@@ -1116,6 +1118,7 @@ def render_html_text(text: str, **kwargs):
|
|||||||
bold: Boolean, whether bold (or not)
|
bold: Boolean, whether bold (or not)
|
||||||
italic: Boolean, whether italic (or not)
|
italic: Boolean, whether italic (or not)
|
||||||
heading: str, heading level e.g. 'h3'
|
heading: str, heading level e.g. 'h3'
|
||||||
|
sanitize: Boolean, whether to sanitize the provided text (default = True)
|
||||||
"""
|
"""
|
||||||
tags = []
|
tags = []
|
||||||
|
|
||||||
@@ -1128,6 +1131,12 @@ def render_html_text(text: str, **kwargs):
|
|||||||
if heading := kwargs.get('heading', ''):
|
if heading := kwargs.get('heading', ''):
|
||||||
tags.append(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 = ''.join([f'<{tag}>' for tag in tags])
|
||||||
output += text
|
output += text
|
||||||
output += ''.join([f'</{tag}>' for tag in tags])
|
output += ''.join([f'</{tag}>' for tag in tags])
|
||||||
|
|||||||
@@ -326,6 +326,17 @@ class ReportTagTest(PartImageTestMixin, InvenTreeTestCase):
|
|||||||
link = report_tags.internal_link(None, None)
|
link = report_tags.internal_link(None, None)
|
||||||
self.assertEqual(link, 'None')
|
self.assertEqual(link, 'None')
|
||||||
|
|
||||||
|
# Unsafe text content is escaped
|
||||||
|
payload = '"><script>alert(1)</script>'
|
||||||
|
escaped = '"><script>alert(1)</script>'
|
||||||
|
|
||||||
|
link = report_tags.internal_link(obj.get_absolute_url(), payload)
|
||||||
|
self.assertIn(f'>{escaped}</a>', link)
|
||||||
|
self.assertNotIn('<script>', link)
|
||||||
|
|
||||||
|
# Unsafe text is also escaped when no link can be constructed
|
||||||
|
self.assertEqual(report_tags.internal_link(None, payload), escaped)
|
||||||
|
|
||||||
def test_logo_image(self):
|
def test_logo_image(self):
|
||||||
"""Unit tests for the 'logo_image' tag."""
|
"""Unit tests for the 'logo_image' tag."""
|
||||||
# In debug mode, a web-accessible URL to the logo is returned
|
# In debug mode, a web-accessible URL to the logo is returned
|
||||||
@@ -965,6 +976,24 @@ class ReportTagTest(PartImageTestMixin, InvenTreeTestCase):
|
|||||||
'<h1>hello world</h1>',
|
'<h1>hello world</h1>',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Unsafe HTML content is sanitized by default
|
||||||
|
payload = (
|
||||||
|
'<b>hello</b><script>alert(1)</script><img src="x" onerror="alert(1)">'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
report_tags.render_html_text(payload, bold=True),
|
||||||
|
'<strong><b>hello</b><img src="x"></strong>',
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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):
|
def test_format_date(self):
|
||||||
"""Test the format_date template tag."""
|
"""Test the format_date template tag."""
|
||||||
dt = timezone.datetime(year=2024, month=3, day=13)
|
dt = timezone.datetime(year=2024, month=3, day=13)
|
||||||
|
|||||||
Reference in new issue
Block a user