From d14b4a6fa1b3e40e15f25d59d5af241b3039953b Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 29 Jul 2026 07:21:19 +0000 Subject: [PATCH] :bug: Escape markdown in Mattermost error notifications Add escape-markdown to common/data.cljc that escapes Markdown special characters (*, _, ~, `, [, ], >, #, @, etc.) by prefixing them with backslash. Apply it to user-controlled fields (:hint, :href) in the Mattermost error reporter before constructing the notification message. This is an internal-only feature not accessible to end users. AI-assisted-by: mimo-v2.5-pro --- backend/src/app/loggers/mattermost.clj | 5 +++-- common/src/app/common/data.cljc | 9 +++++++++ common/test/common_tests/data_test.cljc | 19 +++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/backend/src/app/loggers/mattermost.clj b/backend/src/app/loggers/mattermost.clj index e3089f1f03..82a3c134e6 100644 --- a/backend/src/app/loggers/mattermost.clj +++ b/backend/src/app/loggers/mattermost.clj @@ -7,6 +7,7 @@ (ns app.loggers.mattermost "A mattermost integration for error reporting." (:require + [app.common.data :as d] [app.common.exceptions :as ex] [app.common.logging :as l] [app.common.pprint :as pp] @@ -25,7 +26,7 @@ (defn- send-mattermost-notification! [cfg {:keys [id] :as report}] (let [type (get report :type) - text (str "#" type " | " (get report :hint) "\n" + text (str "#" type " | " (d/escape-markdown (get report :hint)) "\n" (when id (str (u/join (cf/get :public-uri) "/dbg/error/" id) " ")) @@ -38,7 +39,7 @@ "- tenant: #" (:tenant report) "\n" "- origin: #" (:origin report) "\n" (when-let [href (get report :href)] - (str "- href: `" href "`\n")) + (str "- href: `" (d/escape-markdown href) "`\n")) (when-let [version (get report :frontend-version)] (str "- frontend-version: `" version "`\n")) (when-let [version (get report :backend-version)] diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc index 7cbfdcc4f5..a0e2b7b2da 100644 --- a/common/src/app/common/data.cljc +++ b/common/src/app/common/data.cljc @@ -1183,6 +1183,15 @@ str/trim) "")) +(defn escape-markdown + "Escapes Markdown special characters by prefixing them with backslash. + Intended for user-controlled values embedded in Markdown messages + (e.g. Mattermost notifications)." + [s] + (if s + (str/replace (str s) #"([*_~`\[\]()>#+=\-|{}.!@\\])" "\\\\$1") + "")) + (defn get-initials "Returns up to two uppercase initials extracted from a string. Non-letter prefixes in each token are ignored." diff --git a/common/test/common_tests/data_test.cljc b/common/test/common_tests/data_test.cljc index 39f3370de8..e9bccb8fa8 100644 --- a/common/test/common_tests/data_test.cljc +++ b/common/test/common_tests/data_test.cljc @@ -36,6 +36,25 @@ (t/is (= "" (d/get-initials nil))) (t/is (= "" (d/get-initials "!!! ???")))) +(t/deftest escape-markdown-test + (t/is (= "hello" (d/escape-markdown "hello"))) + (t/is (= "" (d/escape-markdown nil))) + (t/is (= "" (d/escape-markdown ""))) + (t/is (= "\\*bold\\*" (d/escape-markdown "*bold*"))) + (t/is (= "\\_italic\\_" (d/escape-markdown "_italic_"))) + (t/is (= "\\~strikethrough\\~" (d/escape-markdown "~strikethrough~"))) + (t/is (= "\\`code\\`" (d/escape-markdown "`code`"))) + (t/is (= "\\[link\\]\\(http://evil\\.com\\)" (d/escape-markdown "[link](http://evil.com)"))) + (t/is (= "\\> quote" (d/escape-markdown "> quote"))) + (t/is (= "\\# heading" (d/escape-markdown "# heading"))) + (t/is (= "\\@channel" (d/escape-markdown "@channel"))) + (t/is (= "\\!bang" (d/escape-markdown "!bang"))) + (t/is (= "normal\\-text" (d/escape-markdown "normal-text"))) + (t/is (= "a\\+b\\=c" (d/escape-markdown "a+b=c"))) + (t/is (= "pipe\\|separated" (d/escape-markdown "pipe|separated"))) + (t/is (= "curly\\{\\}braces" (d/escape-markdown "curly{}braces"))) + (t/is (= "backslash\\\\slash" (d/escape-markdown "backslash\\slash")))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Ordered Data Structures ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;