diff --git a/CHANGELOG.md b/CHANGELOG.md index 8025f9ec..27889203 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ All notable changes to this project will be documented in this file. - When copying a check, copy all fields from the "Filtering Rules" dialog (#417) - Fix missing Resume button (#421) - When decoding inbound emails, decode encoded headers (#420) +- Escape markdown in MS Teams notifications (#426) ## v1.16.0 - 2020-08-04 diff --git a/hc/api/tests/test_notify.py b/hc/api/tests/test_notify.py index 880e79b7..fab6a569 100644 --- a/hc/api/tests/test_notify.py +++ b/hc/api/tests/test_notify.py @@ -866,6 +866,26 @@ class NotifyTestCase(BaseTestCase): payload = kwargs["json"] self.assertEqual(payload["@type"], "MessageCard") + @patch("hc.api.transports.requests.request") + def test_msteams_escapes_markdown(self, mock_post): + self._setup_data("msteams", "http://example.com/webhook") + mock_post.return_value.status_code = 200 + + self.check.name = """ + TEST _underscore_ `backticks` underline \\backslash\\ "quoted" + """ + + self.channel.notify(self.check) + + args, kwargs = mock_post.call_args + text = kwargs["json"]["text"] + + self.assertIn(r"\_underscore\_", text) + self.assertIn(r"\`backticks\`", text) + self.assertIn("<u>underline</u>", text) + self.assertIn(r"\\backslash\\ ", text) + self.assertIn(""quoted"", text) + @patch("hc.api.transports.os.system") @override_settings(SHELL_ENABLED=True) def test_shell(self, mock_system): diff --git a/hc/api/transports.py b/hc/api/transports.py index 0b673aeb..bbe4a3d8 100644 --- a/hc/api/transports.py +++ b/hc/api/transports.py @@ -3,6 +3,7 @@ import os from django.conf import settings from django.template.loader import render_to_string from django.utils import timezone +from django.utils.html import escape import json import requests from urllib.parse import quote, urlencode @@ -582,6 +583,15 @@ class MsTeams(HttpTransport): def notify(self, check): text = tmpl("msteams_message.json", check=check) payload = json.loads(text) + + # Escape special HTML characters in check's name + safe_name = escape(check.name_then_code()) + # Escape characters that have special meaning in Markdown + for c in r"\`*_{}[]()#+-.!|": + safe_name = safe_name.replace(c, "\\" + c) + + payload["text"] = f"“{safe_name}” is {check.status.upper()}." + return self.post(self.channel.value, json=payload) @@ -629,7 +639,5 @@ class LineNotify(HttpTransport): "Content-Type": "application/x-www-form-urlencoded", "Authorization": "Bearer %s" % self.channel.linenotify_token, } - payload = { - "message": tmpl("linenotify_message.html", check=check) - } + payload = {"message": tmpl("linenotify_message.html", check=check)} return self.post(self.URL, headers=headers, params=payload) diff --git a/templates/integrations/msteams_message.json b/templates/integrations/msteams_message.json index 796b16b8..c26b08d6 100644 --- a/templates/integrations/msteams_message.json +++ b/templates/integrations/msteams_message.json @@ -3,7 +3,6 @@ "@type": "MessageCard", "@context": "https://schema.org/extensions", "themeColor": "{% if check.status == "up" %}5cb85c{% endif %}{% if check.status == "down" %}d9534f{% endif %}", - "text": "“{{ check.name_then_code|escapejs }}” is {{ check.status|upper }}.", "sections": [ { "facts": [