🐛 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
This commit is contained in:
Andrey Antukh 2026-07-29 07:21:19 +00:00
parent 319a2185c9
commit d14b4a6fa1
3 changed files with 31 additions and 2 deletions

View File

@ -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)]

View File

@ -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."

View File

@ -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
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;