This is an automated email from the ASF dual-hosted git repository.

gcruz pushed a commit to branch gc/8596
in repository https://gitbox.apache.org/repos/asf/allura.git

commit 0e9324b3cff577c2f0445e4a89b283036f45445c
Author: Guillermo Cruz <[email protected]>
AuthorDate: Fri Feb 13 10:12:34 2026 -0700

    [#8596] escape markdown and also html in notification email templates
---
 Allura/allura/config/app_cfg.py                         |  1 +
 Allura/allura/lib/helpers.py                            | 17 +++++++++++++++++
 Allura/allura/model/notification.py                     |  2 ++
 Allura/allura/tasks/mail_tasks.py                       |  1 -
 Allura/allura/templates/mail/Discussion.txt             |  2 +-
 Allura/allura/templates/mail/MergeRequest.txt           |  2 +-
 Allura/allura/templates/mail/Ticket.txt                 | 14 +++++++-------
 Allura/allura/templates/mail/bulk_export.html           |  6 +++---
 Allura/allura/templates/mail/claimed_existing_email.txt |  2 +-
 Allura/allura/templates/mail/commits.md                 |  4 ++--
 Allura/allura/templates/mail/email_added.md             |  4 ++--
 Allura/allura/templates/mail/email_removed.md           |  4 ++--
 Allura/allura/templates/mail/footer.txt                 |  2 +-
 Allura/allura/templates/mail/monitor_email_footer.txt   |  2 +-
 Allura/allura/templates/mail/password_changed.md        |  4 ++--
 Allura/allura/templates/mail/primary_email_changed.md   |  4 ++--
 Allura/allura/templates/mail/twofactor_disabled.md      |  2 +-
 Allura/allura/templates/mail/twofactor_enabled.md       |  2 +-
 Allura/allura/templates/mail/usermentions_email.md      |  2 +-
 19 files changed, 48 insertions(+), 29 deletions(-)

diff --git a/Allura/allura/config/app_cfg.py b/Allura/allura/config/app_cfg.py
index 01df3da77..db663dbbf 100644
--- a/Allura/allura/config/app_cfg.py
+++ b/Allura/allura/config/app_cfg.py
@@ -190,6 +190,7 @@ def create(cls, config, app_globals):
         jinja2_env.filters['nl2br'] = helpers.nl2br_jinja_filter
         jinja2_env.filters['subrender'] = helpers.subrender_jinja_filter
         jinja2_env.filters['safe_html'] = helpers.clean_html
+        jinja2_env.filters['escape_markdown'] = helpers.escape_markdown
         jinja2_env.globals.update({
             'hasattr': hasattr,
             'h': helpers,
diff --git a/Allura/allura/lib/helpers.py b/Allura/allura/lib/helpers.py
index bc2f8cee2..8e1890168 100644
--- a/Allura/allura/lib/helpers.py
+++ b/Allura/allura/lib/helpers.py
@@ -74,6 +74,7 @@
 import urllib.parse as urlparse
 from urllib.parse import urlencode
 import math
+import markdown
 
 # import to make available to templates, don't delete:
 from .security import has_access, is_allowed_by_role, is_site_admin  # noqa: 
F401 RUF100
@@ -1434,3 +1435,19 @@ def parse_fediverse_address(username: str):
 def clean_html(value: str) -> Markup:
     from allura.lib.markdown_extensions import HTMLSanitizer
     return Markup(HTMLSanitizer().run(value))  # noqa: S704
+
+
+def escape_markdown(content: str) -> str:
+    if content is None:
+        return ''
+    md = markdown.Markdown()
+    escaped_chars = md.ESCAPED_CHARS
+    if isinstance(escaped_chars, (list, tuple, set)):
+        escaped_chars = "".join(escaped_chars)
+    else:
+        escaped_chars = str(escaped_chars)
+    # escape html tag like <b> or <img>
+    html_escaped = html.escape(content, quote=True)
+    pattern = re.compile(rf"([{re.escape(escaped_chars)}])")
+    # Escape markdown special characters to prevent unintended formatting
+    return pattern.sub(r"\\\1", html_escaped)
diff --git a/Allura/allura/model/notification.py 
b/Allura/allura/model/notification.py
index 337411c3d..0cfe7ee93 100644
--- a/Allura/allura/model/notification.py
+++ b/Allura/allura/model/notification.py
@@ -111,6 +111,7 @@ class __mongometa__:
         loader=jinja2.PackageLoader('allura', 'templates'),
         auto_reload=asbool(config.get('auto_reload_templates', True)),
     )
+    view.filters['escape_markdown'] = h.escape_markdown
 
     @classmethod
     def post(cls, artifact, topic, 
additional_artifacts_to_match_subscriptions=None, **kw):
@@ -689,6 +690,7 @@ class MailFooter:
         loader=jinja2.PackageLoader('allura', 'templates'),
         auto_reload=asbool(config.get('auto_reload_templates', True)),
     )
+    view.filters['escape_markdown'] = h.escape_markdown
 
     @classmethod
     def _render(cls, template, **kw):
diff --git a/Allura/allura/tasks/mail_tasks.py 
b/Allura/allura/tasks/mail_tasks.py
index 75523b444..ea97f06b2 100644
--- a/Allura/allura/tasks/mail_tasks.py
+++ b/Allura/allura/tasks/mail_tasks.py
@@ -128,7 +128,6 @@ def replace_html(matchobj):
                         )
     plain_text = html.unescape(plain_text)  # put literal HTML tags back into 
plaintext
     plain_msg = mail_util.encode_email_part(plain_text, 'plain')
-
     html_text = ForgeMarkdown(email=True).convert(text)
     if metalink:
         html_text = html_text + mail_meta_content(metalink)
diff --git a/Allura/allura/templates/mail/Discussion.txt 
b/Allura/allura/templates/mail/Discussion.txt
index 21bfb51f2..f9e66b219 100644
--- a/Allura/allura/templates/mail/Discussion.txt
+++ b/Allura/allura/templates/mail/Discussion.txt
@@ -20,4 +20,4 @@
 
 ---
 
-[{{post.thread.subject|e}}]({{h.absurl(post.url_paginated())}})
+[{{post.thread.subject|escape_markdown}}]({{h.absurl(post.url_paginated())}})
diff --git a/Allura/allura/templates/mail/MergeRequest.txt 
b/Allura/allura/templates/mail/MergeRequest.txt
index 3302abaa1..3ade1bdaa 100644
--- a/Allura/allura/templates/mail/MergeRequest.txt
+++ b/Allura/allura/templates/mail/MergeRequest.txt
@@ -20,6 +20,6 @@
 
 ---
 
-{{ data.creator_name }} has requested to merge changes from `{{ 
data.downstream_repo.clone_url_first(anon=False) }}` at commit 
`{{data.downstream_repo.shorthand_for_commit(data.downstream.commit_id)}}` into 
the branch `{{ data.target_branch }}`
+{{ data.creator_name|escape_markdown }} has requested to merge changes from 
`{{ data.downstream_repo.clone_url_first(anon=False) }}` at commit 
`{{data.downstream_repo.shorthand_for_commit(data.downstream.commit_id)}}` into 
the branch `{{ data.target_branch|escape_markdown }}`
 
 {{ data.description }}
diff --git a/Allura/allura/templates/mail/Ticket.txt 
b/Allura/allura/templates/mail/Ticket.txt
index fc5b4ae30..e73d9e5cc 100644
--- a/Allura/allura/templates/mail/Ticket.txt
+++ b/Allura/allura/templates/mail/Ticket.txt
@@ -20,25 +20,25 @@
 
 ---
 
-**[{{data.app_config.options.mount_point}}:#{{data.ticket_num}}] 
{{data.summary|e}}**
+**[{{data.app_config.options.mount_point}}:#{{data.ticket_num}}] 
{{data.summary|escape_markdown}}**
 
-**Status:** {{data.status}}
+**Status:** {{data.status|escape_markdown}}
 {% for f in data.globals.milestone_fields -%}
-  **{{ f.label }}:** {{ data.custom_fields.get(f.name, '') }}
+  **{{ f.label|escape_markdown }}:** {{ data.custom_fields.get(f.name, 
'')|escape_markdown }}
 {% endfor -%}
 {% if data.labels.__len__() -%}
-    **Labels:** {% for label in data.labels %}{{label}} {% else %}None{% 
endfor %}
+    **Labels:** {% for label in data.labels %}{{label|escape_markdown}} {% 
else %}None{% endfor %}
 {% endif -%}
-**Created:** {{data.created_date.strftime('%a %b %d, %Y %I:%M %p UTC')}} by 
{{data.reported_by.display_name}}
+**Created:** {{data.created_date.strftime('%a %b %d, %Y %I:%M %p UTC')}} by 
{{data.reported_by.display_name|escape_markdown}}
 {% if (data.mod_date - data.created_date).days >= 0 -%}
     **Last Updated:** {{data.mod_date.strftime('%a %b %d, %Y %I:%M %p UTC')}}
 {% endif -%}
-**Owner:** {{data.assigned_to_name()}}
+**Owner:** {{data.assigned_to_name()|escape_markdown}}
 {% if data.attachments -%}
     **Attachments:**
 
 {% for att in data.attachments -%}
-    - [{{att.filename}}]({{h.absurl(att.url())}}) 
({{h.do_filesizeformat(att.length)}}; {{att.content_type}})
+    - [{{att.filename|escape_markdown}}]({{h.absurl(att.url())}}) 
({{h.do_filesizeformat(att.length)}}; {{att.content_type}})
 {% endfor -%}
 {% endif %}
 
diff --git a/Allura/allura/templates/mail/bulk_export.html 
b/Allura/allura/templates/mail/bulk_export.html
index e8aded10f..b135041ac 100644
--- a/Allura/allura/templates/mail/bulk_export.html
+++ b/Allura/allura/templates/mail/bulk_export.html
@@ -17,19 +17,19 @@
        under the License.
 #}
 
-The bulk export for project {{ project.shortname }} is completed.
+The bulk export for project {{ project.shortname|escape_markdown }} is 
completed.
 
 {% if tools %}
 The following tools were exported:
 {% for tool in tools -%}
-- {{ tool }}
+- {{ tool|escape_markdown }}
 {% endfor %}
 {% endif %}
 
 {% if not_exported_tools %}
 The following tools were not exported:
 {% for tool in not_exported_tools -%}
-- {{ tool }}
+- {{ tool|escape_markdown }}
 {% endfor %}
 {% endif %}
 
diff --git a/Allura/allura/templates/mail/claimed_existing_email.txt 
b/Allura/allura/templates/mail/claimed_existing_email.txt
index ead38a808..cda4ceca9 100644
--- a/Allura/allura/templates/mail/claimed_existing_email.txt
+++ b/Allura/allura/templates/mail/claimed_existing_email.txt
@@ -17,6 +17,6 @@
        under the License.
 #}
 
-You tried to add {{ email.email }} to your {{ config['site_name'] }} account, 
but it is already claimed by your {{ user.username }} account.
+You tried to add {{ email.email }} to your {{ config['site_name'] }} account, 
but it is already claimed by your {{ user.username|escape_markdown }} account.
 You should use that account instead, or remove that address from that account.
 If this was not you who attempted this, you can safely ignore this email.
diff --git a/Allura/allura/templates/mail/commits.md 
b/Allura/allura/templates/mail/commits.md
index 1b1d043c4..e61b5b3a7 100644
--- a/Allura/allura/templates/mail/commits.md
+++ b/Allura/allura/templates/mail/commits.md
@@ -18,11 +18,11 @@
 --> #}
 {%- for cm in commit_msgs[:max_num_commits] %}
 {%- if cm.branches and cm.show_branch_name|default %}
-## Branch: {% for b in cm.branches %}{{ b }}  {% endfor%}
+## Branch: {% for b in cm.branches %}{{ b|escape_markdown }}  {% endfor%}
 {% endif %}
 {{ cm.summary }}
 
-By {{ cm.author }} on {{ cm.date }}
+By {{ cm.author|escape_markdown }} on {{ cm.date }}
 [**View Changes**]({{ cm.commit_url }})
 
 {% if not loop.last %}-----{% endif %}
diff --git a/Allura/allura/templates/mail/email_added.md 
b/Allura/allura/templates/mail/email_added.md
index eace71880..3090c43ff 100644
--- a/Allura/allura/templates/mail/email_added.md
+++ b/Allura/allura/templates/mail/email_added.md
@@ -17,9 +17,9 @@
        under the License.
 -#}
 
-Hello {{ user.display_name }},
+Hello {{ user.display_name|escape_markdown }},
 
-A new email address was added to your {{ config['site_name'] }} account "{{ 
user.username }}":  
+A new email address was added to your {{ config['site_name'] }} account "{{ 
user.username|escape_markdown }}":  
 
 * {{ addr }}
 
diff --git a/Allura/allura/templates/mail/email_removed.md 
b/Allura/allura/templates/mail/email_removed.md
index 323ea17e2..2758eca70 100644
--- a/Allura/allura/templates/mail/email_removed.md
+++ b/Allura/allura/templates/mail/email_removed.md
@@ -17,9 +17,9 @@
        under the License.
 -#}
 
-Hello {{ user.display_name }},
+Hello {{ user.display_name|escape_markdown }},
 
-An email address was removed from your {{ config['site_name'] }} account "{{ 
user.username }}":  
+An email address was removed from your {{ config['site_name'] }} account "{{ 
user.username|escape_markdown }}":  
 
 * {{ addr }} 
 
diff --git a/Allura/allura/templates/mail/footer.txt 
b/Allura/allura/templates/mail/footer.txt
index 62de8623f..08a7e08b8 100644
--- a/Allura/allura/templates/mail/footer.txt
+++ b/Allura/allura/templates/mail/footer.txt
@@ -20,7 +20,7 @@
 
 ---
 
-Sent from {{domain}} because you indicated interest in <{{ prefix }}{{ 
notification.link }}>
+Sent from {{domain|escape_markdown}} because you indicated interest in <{{ 
prefix }}{{ notification.link }}>
 
 {% if discussion_disabled %}
 Please do not reply to this message. Discussion is disabled for the <{{ prefix 
}}{{ notification.link }}>
diff --git a/Allura/allura/templates/mail/monitor_email_footer.txt 
b/Allura/allura/templates/mail/monitor_email_footer.txt
index d3492ca37..f2290e12c 100644
--- a/Allura/allura/templates/mail/monitor_email_footer.txt
+++ b/Allura/allura/templates/mail/monitor_email_footer.txt
@@ -20,6 +20,6 @@
 
 ---
 
-Sent from {{domain}} because {{email}} is subscribed to {{app_url}}
+Sent from {{domain}} because {{email|escape_markdown}} is subscribed to 
{{app_url}}
 
 To unsubscribe from further messages, a project admin can change settings at 
{{setting_url}}.  Or, if this is a mailing list, you can unsubscribe from the 
mailing list.
diff --git a/Allura/allura/templates/mail/password_changed.md 
b/Allura/allura/templates/mail/password_changed.md
index 10e062ff0..db6cc4059 100644
--- a/Allura/allura/templates/mail/password_changed.md
+++ b/Allura/allura/templates/mail/password_changed.md
@@ -17,9 +17,9 @@
        under the License.
 -#}
 
-Hello {{ user.display_name }},
+Hello {{ user.display_name|escape_markdown }},
 
-The password for your {{ config['site_name'] }} account "{{ user.username }}" 
has been changed.  This is a confirmation email, you are all set.
+The password for your {{ config['site_name'] }} account "{{ 
user.username|escape_markdown }}" has been changed.  This is a confirmation 
email, you are all set.
 
 {% block footer %}
 If you did not do this, please contact us immediately.
diff --git a/Allura/allura/templates/mail/primary_email_changed.md 
b/Allura/allura/templates/mail/primary_email_changed.md
index 2830b1c2f..fd6dec35b 100644
--- a/Allura/allura/templates/mail/primary_email_changed.md
+++ b/Allura/allura/templates/mail/primary_email_changed.md
@@ -17,9 +17,9 @@
        under the License.
 -#}
 
-Hello {{ user.display_name }},
+Hello {{ user.display_name|escape_markdown }},
 
-The primary email address on your {{ config['site_name'] }} account "{{ 
user.username }}" was changed to:  
+The primary email address on your {{ config['site_name'] }} account "{{ 
user.username|escape_markdown }}" was changed to:  
 
 * {{ addr }}
 
diff --git a/Allura/allura/templates/mail/twofactor_disabled.md 
b/Allura/allura/templates/mail/twofactor_disabled.md
index 3e4e619b0..e7e0a868f 100644
--- a/Allura/allura/templates/mail/twofactor_disabled.md
+++ b/Allura/allura/templates/mail/twofactor_disabled.md
@@ -17,7 +17,7 @@
        under the License.
 -#}
 
-Hello {{ user.username }},
+Hello {{ user.username|escape_markdown }},
 
 You have recently disabled two-factor authentication on {{ config['site_name'] 
}}.  :(
 
diff --git a/Allura/allura/templates/mail/twofactor_enabled.md 
b/Allura/allura/templates/mail/twofactor_enabled.md
index e2d0ca06f..6e0734349 100644
--- a/Allura/allura/templates/mail/twofactor_enabled.md
+++ b/Allura/allura/templates/mail/twofactor_enabled.md
@@ -17,7 +17,7 @@
        under the License.
 -#}
 
-Hello {{ user.username }},
+Hello {{ user.username|escape_markdown }},
 
 You have recently set up new two-factor authentication on {{ 
config['site_name'] }}.  This is a confirmation email, you are all set.
 
diff --git a/Allura/allura/templates/mail/usermentions_email.md 
b/Allura/allura/templates/mail/usermentions_email.md
index 98763ca69..836c315a1 100644
--- a/Allura/allura/templates/mail/usermentions_email.md
+++ b/Allura/allura/templates/mail/usermentions_email.md
@@ -16,7 +16,7 @@
        specific language governing permissions and limitations
        under the License.
 -#}
-Your name was mentioned at [{{artifact_linktext}}]({{artifact_link}}) by 
{{mentioned_by.display_name}}
+Your name was mentioned at 
[{{artifact_linktext|escape_markdown}}]({{artifact_link}}) by 
{{mentioned_by.display_name|escape_markdown}}
 
 It can be viewed using the link below:
 {{artifact_link}}

Reply via email to