fix: escape notification HTML device fields

Co-authored-by: jokob-sk <96159884+jokob-sk@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]andjokob-sk authored and GitHub committed 2026-08-14 15:29:44 +00:00
1 parent 208fa928ea
commit 0197e7c2cf
2 files changed
+92 -15

No files matched your search

+42 -15
View File
@@ -1,8 +1,10 @@
import html
import json
import re
import uuid
import socket
from yattag import indent
from yattag.indentation import XMLTokenError
from json2table import convert
# Register NetAlertX modules
@@ -146,20 +148,7 @@ class NotificationInstance:
mail_html, conf.REPORT_DASHBOARD_URL + "/deviceDetails.php?mac="
)
# Add preheader for inbox preview after all links have been generated.
# Invisible padding prevents email clients from showing the start of the email body.
preheader = "".join(preheaders)
padding = (" &zwnj;&#8199;" * 47)
mail_html = mail_html.replace(
"PREHEADER",
preheader + padding,
)
final_html = indent(
mail_html, indentation=" ", newline="\r\n", indent_text=True
)
final_html = finalize_html(mail_html, preheaders)
send_api(self.JSON, final_text, final_html)
@@ -335,8 +324,9 @@ def construct_notifications(JSON, section):
text = tableTitle + "\n---------\n"
# Convert a JSON into an HTML table
html_data = escape_html_rows(jsn)
html = convert(
{"data": jsn},
{"data": html_data},
build_direction=build_direction,
table_attributes=table_attributes,
)
@@ -398,6 +388,43 @@ def format_table(html, thValue, props, newThValue=""):
)
# -----------------------------------------------------------------------------
# Escape free-text values before embedding them into notification HTML
def escape_html_rows(rows):
return [
{
key: html.escape(value) if isinstance(value, str) else value
for key, value in row.items()
}
for row in rows
]
# -----------------------------------------------------------------------------
# Finalize HTML and tolerate pretty-print failures
def finalize_html(mail_html, preheaders):
# Add preheader for inbox preview after all links have been generated.
# Invisible padding prevents email clients from showing the start of the email body.
preheader = html.escape("".join(preheaders))
padding = (" &zwnj;&#8199;" * 47)
mail_html = mail_html.replace(
"PREHEADER",
preheader + padding,
)
try:
return indent(
mail_html, indentation=" ", newline="\r\n", indent_text=True
)
except XMLTokenError as err:
mylog(
"warn",
f"[Notification] Failed to pretty-print HTML report, sending unindented HTML instead: {err}",
)
return mail_html
# -----------------------------------------------------------------------------
# Pre-header Preview
def build_preheader(tableTitle, jsn, headers):
@@ -295,6 +295,56 @@ class TestConstructNotificationsTemplates(unittest.TestCase):
self.assertEqual(html_without, html_with)
# -----------------------------------------------------------------
# HTML output escapes free-text device values while text stays raw
# -----------------------------------------------------------------
@patch("models.notification_instance.get_setting_value")
def test_html_escapes_free_text_values(self, mock_setting):
from models.notification_instance import construct_notifications
mock_setting.side_effect = self._setting_factory({
"NTFPRCS_TEXT_SECTION_HEADERS": True,
"NTFPRCS_TEXT_TEMPLATE_new_devices": "",
})
devices = [
{
"devName": "Meta Quest <Pro",
"eveMac": "aa:bb:cc:dd:ee:ff",
"devVendor": "Meta",
"eveIp": "192.168.1.42",
"eveDateTime": "2025-01-15 10:30:00",
"eveEventType": "New Device",
"devComments": "values <=2 break things",
}
]
json_data = _make_json(
"new_devices", devices, NEW_DEVICE_COLUMNS, "🆕 New devices"
)
html, text, _ = construct_notifications(json_data, "new_devices")
self.assertIn("Meta Quest &lt;Pro", html)
self.assertIn("values &lt;=2 break things", html)
self.assertNotIn("Meta Quest <Pro", html)
self.assertNotIn("values <=2 break things</td>", html)
self.assertIn("Meta Quest <Pro", text)
self.assertIn("values <=2 break things", text)
# -----------------------------------------------------------------
# Final HTML escapes preheaders and tolerates indent failures
# -----------------------------------------------------------------
def test_finalize_html_escapes_preheader_and_falls_back(self):
from models.notification_instance import finalize_html
final_html = finalize_html(
"<html><body><span>PREHEADER</span>broken < content</body></html>",
["Meta Quest <Pro"],
)
self.assertIn("Meta Quest &lt;Pro", final_html)
self.assertIn("broken < content", final_html)
if __name__ == "__main__":
unittest.main()