feat(alerts): add opt-in "Allow HTML content" for custom templates#7765
feat(alerts): add opt-in "Allow HTML content" for custom templates#7765Darian-Morrison wants to merge 3 commits into
Conversation
Values interpolated into a custom alert subject/body (query results and other {{...}} variables) are HTML-escaped by default. That is correct for the HTML email body but turns links and markup from query results into literal text in other destinations. Add an "Allow HTML content" checkbox to the alert template editor, mirroring the table visualization's per-column toggle, that renders the custom template without escaping.
Escaping stays on by default, and the flag only affects the author's custom template -- the system default mail body is always escaped.
There was a problem hiding this comment.
2 issues found across 6 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="redash/models/__init__.py">
<violation number="1" location="redash/models/__init__.py:1084">
P1: When `allow_html` is enabled, `render_template` switches the entire template from the escaping renderer to raw Mustache for all variables—including `QUERY_RESULT_VALUE`, `QUERY_RESULT_ROWS`, and other query-derived context—without visible sanitization or destination-specific controls. Since query results may contain attacker-controlled data, an alert owner opting in to HTML for Slack `mrkdwn` could inadvertently inject executable markup into email or web UI notifications that other subscribers receive. Consider scoping the raw rendering to intended variables or adding downstream sanitization so the opt-in does not become a cross-channel XSS vector.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| "QUERY_RESULT_TABLE": result_table, | ||
| } | ||
| return mustache_render_escape(template, context) | ||
| render = mustache_render_escape if escape else mustache_render |
There was a problem hiding this comment.
P1: When allow_html is enabled, render_template switches the entire template from the escaping renderer to raw Mustache for all variables—including QUERY_RESULT_VALUE, QUERY_RESULT_ROWS, and other query-derived context—without visible sanitization or destination-specific controls. Since query results may contain attacker-controlled data, an alert owner opting in to HTML for Slack mrkdwn could inadvertently inject executable markup into email or web UI notifications that other subscribers receive. Consider scoping the raw rendering to intended variables or adding downstream sanitization so the opt-in does not become a cross-channel XSS vector.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At redash/models/__init__.py, line 1084:
<comment>When `allow_html` is enabled, `render_template` switches the entire template from the escaping renderer to raw Mustache for all variables—including `QUERY_RESULT_VALUE`, `QUERY_RESULT_ROWS`, and other query-derived context—without visible sanitization or destination-specific controls. Since query results may contain attacker-controlled data, an alert owner opting in to HTML for Slack `mrkdwn` could inadvertently inject executable markup into email or web UI notifications that other subscribers receive. Consider scoping the raw rendering to intended variables or adding downstream sanitization so the opt-in does not become a cross-channel XSS vector.</comment>
<file context>
@@ -1081,17 +1081,18 @@ def render_template(self, template):
"QUERY_RESULT_TABLE": result_table,
}
- return mustache_render_escape(template, context)
+ render = mustache_render_escape if escape else mustache_render
+ return render(template, context)
</file context>
There was a problem hiding this comment.
This is why the option is opt-in and has a warning tooltip.
options is stored as raw JSONB, so a truthy non-boolean value (e.g. the string "false") could otherwise disable escaping. Gate on `is True` so only an explicit boolean opt-in renders the custom template unescaped.
What type of PR is this?
Description
In a custom alert subject/body, values interpolated from query results (
{{QUERY_RESULT_VALUE}}, table rows, and other{{...}}variables) are HTML-escaped by default. This prevents dynamic injection of HTML content into Alert bodies.This adds an "Allow HTML content" checkbox to the alert template editor (Inspired by the table visualization per-column toggle) that renders the custom template without escaping.
For our purposes, this gives Slack destined alerts
mrkdwnembedding allowing dynamic user tagging and hyperlinking.How is this tested?
Added
tests/models/test_alerts.pycases: escape-by-default, raw rendering when enabled(subject + body), and that a direct
render_template()call stays escaped. Manuallyverified in the alert editor and against a rendered alert.
Related Tickets & Documents
N/A
Mobile & Desktop Screenshots/Recordings (if there are UI changes)